Символы кириллицы выводит некорректно. Как сделать чтобы выводился читабельный текст на русском языке? Тип приложения - не Qt, Qt Creator 4.5.0 основан на Qt 5.10.0. Win7.
|
Объектно-ориентированное программирование
10.3 Создание и удаление объектов
10.3.1 Присваивание объектов, передача в функцию и возвращение объекта
Можно заметить, что в ряде случаев (например, при перегрузке операторов) мы использовали передачу объекта в функцию и возвращение не по значению, а с использованием ссылки. Для этого существует достаточно веская причина: передача объектов по значению, равно как и их присваивание, может приводить к нежелательным последствиям.
Один объект можно присвоить другому, если оба объекта имеют одинаковый тип (если объекты имеют разные типы, то компилятор выдаст сообщение об ошибке). По умолчанию, когда объект A присваивается объекту B, то осуществляется побитовое копирование всех элементов-данных A в соответствующие элементы-данные B. Именно это копирование и является потенциальным источником проблем. Особенно внимательным нужно быть при присваивании объектов, имеющих свойства-указатели.
Рассмотрим в качестве примера класс matrix, хранящий в себе прямоугольную матрицу из элементов типа double. Размерность матрицы будет передаваться конструктору класса, после чего будет выполняться динамическое выделение памяти под нужное количество элементов. В классе будут также предусмотрены методы get_val() чтобы получить элемент матрицы с индексами (i,j) и set_val() чтобы установить в заданный элемент новое значение.
Однако присвоив просто так одну переменную типа matrix другой, мы не сможем избежать побочных эффектов.
#include <iostream> using namespace std; class matrix { double *m; //элементы матрицы size_t width, height; //число строк и столбцов в матрице public : matrix ( size_t w, size_t h ); double get_val ( size_t i, size_t j ); void set_val ( size_t i, size_t j, double val ); ˜matrix ( ); }; matrix::matrix ( size_t w, size_t h ) { m = new double [w*h ]; width = w; height = h; } matrix::˜ matrix ( ) { delete [ ] m; } double matrix::get_val ( size_t i, size_t j ) { return m[ i * width+j ]; //получить значение элемента матрицы в позиции [i,j] } void matrix : :set_val ( size_t i, size_t j, double val ) { //устанавливаем значение элемента матрицы в позиции [i,j] //если координаты не превышают размер матрицы if ( ( i<width )&&(j<height ) ) m[ i * width+j ]= val; } main ( ) { matrix a(2, 2 ); //объявляем матрицу размерности 2 х 2 a.set_val ( 0, 0, 100 ); //устанавливаем a[0,0] = 100 matrix b=a;//присваиваем матрицу b.set_val ( 0, 0, 200 ); //устанавливаем b[0,0] = 200 cout << " a[0, 0 ] = " << a.get_val ( 0, 0 ) << "; " << " b[ 0, 0 ] = " << b.get_val ( 0, 0 ) << endl; }
При запуске программа выдаёт сообщение "a[0,0] = 200; b[0,0] = 200" вместо ожидаемого "a[0,0]=100", после чего и вовсе аварийно завершается с сообщением о попытке дважды освободить память. На самом деле это происходит по вполне очевидной причине. При побитовом копировании скопировался адрес указателя m, а не содержимое блока памяти, динамически выделенного по этому адресу. В результате оба объекта получают указатель на одну и ту же последовательность вещественных чисел.
Аналогично присваиванию, объекты можно передавать в функции в качестве аргументов, в точности так, как передаются данные других типов. Однако следует помнить, что в C++ по-умолчанию параметры передаются по значению. Это означает, что внутри функции (а точнее, в стеке) создаётся копия объекта-аргумента, и эта копия, а не сам объект, будет далее использоваться функцией. Благодаря этому функции могут произвольно изменять переданные значения, не влияя на оригинал.
Итак, при передаче объекта в функцию создаётся новый объект, а когда работа функции завершается, копия переданного объекта будет разрушена. Как всегда при разрушении объектов, при этом будет вызван деструктор копии. И здесь может наблюдаться очередной побочный эффект: если переданный в качестве параметра объект содержит в себе указатель на динамически выделенную область памяти, деструктор копии её освободит. Но так как копия создавалась побитовым копированием, деструктор копии высвободит область памяти, на которую указывал объект-оригинал. Исходный объект будет по-прежнему "видеть" свои данные по указанному адресу, однако для системы эта память будет считаться свободной. Рано или поздно она будет выделена какому-то другому объекту, и данные окажутся затёрты.
Кроме возможности преждевременного разрушения объекта-оригинала, к аварийной ситуации приведёт вызов его деструктора (в конце работы программы или при выходе из соответствующей области видимости), который попытается освободить уже свободную память. Та же проблема возникает при использовании объекта в качестве возвращаемого значения.
Во всех трёх случаях (при присваивании объекта, при использовании его как параметра и при передаче в качестве возвращаемого значения) если деструктор высвобождает динамически выделенную память, то разрушение временного объекта приведёт к преждевременному разрушению данных оригинала.
Частично проблема может быть решена перегрузкой оператора присваивания для данного класса. Кроме того, для объектов, которым противопоказано побитовое копирование, рекомендуется создавать особую разновидность конструктора — т. н. конструктор копирования (в некоторых источниках также можно встретить название "конструктор копии"). Конструктор копирования выполняет именно то действие, которое заложено в его названии: позволяет программисту лично проконтролировать процесс создания копии.
Любой конструктор копирования имеет следующую форму:
имя_класса ( const имя_класса & obj ) { ... //тело конструктора }
Читатель должен помнить, что в таком описании &obj — это ссылка на объект, известная ещё как скрытый указатель.
Оператор присваивания, перегруженный как член класса, связан со своим классом настолько же тесно, как конструктор и деструктор. Эту связь подчёркивает то, что оператор копирования разрешено перегружать только как функциючлен класса, и запрещено — как дружественную функцию. Приведём в качестве иллюстрации две почти одинаковые записи:
point p1, p2; //объявляем два объекта класса point point p3 = p2; //используем конструктор копирования p1 = p2; //используем оператор присваивания
Во второй строке примера переменная p3 и объявляется и определяется, а в третьей строке переменной p1 всего лишь присваивается значение. Иными словами, конструктор копирования вызывается для конкретной переменной за время её жизни только один раз, а присваивать значения ей можно многократно. В логике работы конструктора копирования и оператора присваивания настолько много общего, что часто рекомендуют описывать одну операцию в терминах другой. Фактически операция присваивания неявно используется в конструкторе копирования. Однако конструктор копирования может добавлять дополнительные действия по инициализации переменных в довесок к тем действиям, которые должен выполнять оператор присваивания.
Если оператор присваивания для класса не был определён, то в случае необходимости (если для объектов этого класса в тексте программы выполняется присваивание) компилятор автоматически генерирует оператор присваивания по умолчанию, выполняющий то самое побитовое копирование объекта.
10.3.2 Подробнее об указателях и ссылках на объекты
Передача объектов по указателю имеет ряд преимуществ. В стек копируется только ячейка памяти, содержащая адрес объекта, и автоматически исчезает проблема корректного создания копии. Конечно в результате функция взаимодействует с оригиналом объекта, что требует осторожного изменения его данных (а с копией можно делать всё что угодно). Ещё одно дополнительное преимущество передачи по адресу в сравнении с передачей по значению — экономия ресурсов на копирование. Если объект занимает достаточно большой объём памяти, его передача в функцию и возвращение из неё, даже будучи выполнены корректно, приведут к неоправданным расходам ресурсов.
Недостаток передачи по указателю — худшая читаемость программы, когда приходится часто взаимодействовать с адресами объектов. Многочисленные операции адресации и разадресации (взятия адреса объекта и взятия содержимого по адресу) могут ухудшать визуальное восприятие текста программы, особенно в сочетании со скобками.
По этой причине в C++ был введён специальный тип данных — ссылка или скрытый указатель. На понятийном уровне ссылку можно воспринимать как другое имя (псевдоним) переменной. Фактически же это указатель на переменную, который выглядит так, как будто к переменной обращаются по значению: программист объявляет такую ссылку, присваивает ей какую-либо переменную и далее пользуется ссылкой как ещё одной переменной. Компилятор же сам автоматически подставляет ко всем обращениям к ссылке операции адресации и разадресации.
Удобнее всего использовать ссылки для передачи параметров и возвращаемых значений.
Напомним: ссылка объявляется так же как указатель, только с использованием знака "" вместо "звёздочки". Сравним, как выглядит код при передаче аргумента по указателю и по ссылке, на примере функции zero(), устанавливающей в ноль координаты переданного ей объекта класса point:
//использование указателей void zero ( point *p ) { p->set ( 0, 0 ); //мы использовали "->" } main ( ) { point a(3, 4 ); zero (&a); } |
//использование ссылки void zero ( point &p ) { p.set ( 0, 0 ); //мы использовали " ." } main ( ) { point a(3, 4 ); zero ( a ); } |
В приведённом примере при применении параметра-ссылки компилятор передаёт адрес переменной, но везде кроме объявления функции код выглядит так, как будто переменная передана по значению. Аналогично ссылки могут использоваться в качестве возвращаемого значения функции. Однако нельзя забывать, что функция, в которую передан параметр по ссылке, будет манипулировать не копией, а самим оригинальным объектом.
Часто ссылки применяют в сочетании с указателем this. Рассмотрим в качестве примера переопределение оператора присваивания для класса point:
point& point::operator= ( point& p ) { x = p.x; y = p.y; return *this; }
В объявлении функции мы указали ссылочный тип в качестве как аргумента, так и возвращаемого значения. Оператор присваивания должен возвращать результат операции, чтобы стало возможным каскадное присваивание наподобие a=b=c=0. В качестве возвращаемого значения мы указываем разадресованный указатель this, однако возвращён в качестве результата будет тот объект, который вызывал операцию "=", а не его копия.
Приведём модифицированный вариант класса matrix, имеющий как конструктор копирования, так и оператор присваивания, и выдающий на экран правильный результат.
#include <iostream> using namespace std; class matrix { double *m; //элементы матрицы size_t width, height; //число строк и столбцов в матрице public : matrix ( size_t w, size_t h ); matrix ( const matrix& m1); //конструктор копирования matrix& operator=(matrix & m1); //оператор присваивания double get_val ( size_t i, size_t j ); void set_val ( size_t i, size_t j, double val ); ˜matrix ( ); }; matrix::matrix ( size_t w, size_t h ) { m = new double [w*h ]; width = w; height = h; } matrix::matrix ( const matrix& m1) { //устанавливаем размер матрицы и выделяем под неё память: width = m1.width; height = m1.height; int size=width*height; m = new double [ size ]; //копируем элементы матрицы: for ( int i =0; i < size; i++) m[ i ]=m1.m[ i ]; } matrix& matrix::operator=(matrix& m1) { int size=m1.width*m1.height; if ( size > width* height ) //защищаемся от переполнения буфера size=width * height; m = new double [ size ]; //копируем элементы матрицы: for ( int i =0; i < size; i++) m[ i ]=m1.m[ i ]; return * this; } matrix::˜ matrix ( ) { delete [ ] m; } double matrix::get_val ( size_t i, size_t j ) { //получить значение элемента матрицы в позиции [i,j] return m[ i * width+j ]; } void matrix : :set_val ( size_t i, size_t j, double val ) { //устанавливаем значение элемента матрицы в позиции [i,j]... //...если координаты не превышают размер матрицы if ( ( i<width )&&(j<height ) ) m[ i * width+j ]= val; } main ( ) { matrix a(2, 2 ); //объявляем матрицу размерности 2 х 2 a.set_val ( 0, 0, 1 0 0 ); //устанавливаем a[0,0] = 100 matrix b=a;//присваиваем матрицу b.set_val ( 0, 0, 2 0 0 ); //устанавливаем b[0,0] = 200 cout << " a[0, 0 ] = " << a.get_val ( 0, 0 ) << "; " << " b[ 0, 0 ] = " << a.get_val ( 0, 0 ) << endl; }
Внимательный читатель может заметить в коде примера необычную особенность. И конструктор копирования, и оператор присваивания получают доступ к закрытой части переданного объекта m1. На самом деле это вполне естественно. Вспомним: переменные, объявленные в закрытой секции класса, доступны только для методов этого же класса (а не "этого же объекта"). Иными словами, объекты одного класса могут получать доступ к закрытым членам друг друга, хотя используется это не так часто.