L10_1 Деструкторы Конструкторы копирования.ppt
- Количество слайдов: 10
Деструктор и конструктор копирования • При создании сложных объектов, чей размер определяется во время исполнения программы (то есть динамически), возникает потребность описать не только его рождение, но и смерть. • Cоздадим двухмерный массив, размеры которого можно задавать в процессе выполнения программы, и чтобы этот новый массив легко передавался функциям независимо от того, сколько в нем строк и столбцов. • Если размеры массива заранее не известны, нужно научиться выделять память динамически, в C++ есть для этого специальный оператор new. • Следующие инструкции создают сначала указатель на double, а затем засылают по этому указателю адрес начала последовательности из двадцати переменных типа double: double *m; m = new double [20]; • Выделим оператором new память для одномерного массива и разместим в нем наш двухмерный массив — строка за строкой.
• Конструктор класса matrix public: matrix(int r = 2, int c = 2) { nrows_ = r; ncols_ = c; m_ = new double[r*c]; } • Конструктор имеет два параметра — число строк r и число столбцов с. Значения (int r=2, int c=2), показанные в списке, нужны при создании массива без явно указанных размеров (например, инструкцией matrix а; ). Указание параметров по умолчанию позволяет иметь один конструктор, создающий массив либо заданных размеров (если они указаны), либо размером 2 x 2. • Оператор new выделяет для массива память, вмещающую г*с переменных double. • Двухмерный массив хранится в памяти построчно. Поэтому элемент, хранящийся в r-й строке и с-м столбце, занимал бы позицию г * ncols + с в одномерном массиве.
• • • В этой программе сначала создается объект darray - двухмерный массив из 200 строк и 100 столбцов, затем каждому элементу присваиваются начальные значения, равные произведению номера строки на номер столбца. Далее созданный массив передается функции foo(), выводящей на экран содержимое элемента (10, 9). Наконец, после возврата из функции на экран выводится «крайний» элемент из последней строки и последнего столбца, равный 99 х 199, то есть 19 701. #include
• • • Конструктор, описанный в классе matrix (см. листинг 8. 11), вызывается в нашей программе всего один раз - потому что массив darray создается в функции main(). Далее созданный массив передается по значению функции foo(). В этом случае функция получает копию передаваемого объекта. Эту копию создает специальный конструктор копирования. Но если он не описан в соответствующем классе, вызывается конструктор копирования по умолчанию, который передает функции копии всех элементов объекта. В нашем случае передадутся копии числа строк nrows, числа столбцов ncols и копия *m - указателя на область памяти, где хранятся элементы массива. Эти копии уничтожатся после выхода из функции, так что программа должна работать нормально. Но представим себе, что объект типа matrix создан внутри функции foo(). Тогда при его объявлении создадутся две целочисленные переменные, хранящие число строк и столбцов, и вызовется конструктор, который выделит с помощью оператора new память для двухмерного массива. Адрес начала этой памяти будет хранить указатель m_.
• Перед завершением работы функции будут уничтожены все эти три объекта (число строк, столбцов и указатель на область памяти, занимаемую массивом), но сама память, выделенная массиву, останется занятой, потому что функция владеет только указателем на эту память, но не ей самой! Произойдет так называемая утечка памяти. • С каждым вызовом foo() свободной памяти, доступной программе, будет все меньше, в конце концов она кончится и программа аварийно завершится. Как же спастись в этом случае? Очевидно, объекту нужен деструктор, освобождающий память, выделенную внутри функции foo(). Нужно сказать, что деструктор по умолчанию у объекта есть всегда. Но в нашем случае нужен специально заданный деструктор, который освободит выделенную для объекта память. Выглядит он как собственная функция с таким же именем, как у класса, но предваренным тильдой (~matrix()). Внутри нашего деструктора всего одна инструкция освобождения памяти (delete [] m_; ), на которую указывает m_. Все вместе это выглядит следующим образом: • • • class matrix { public: ~matrix() { delete [] m_; } private: … }
• • Деструктор не имеет ни параметров, ни возвращаемого значения и вызывается автоматически, когда истекает время жизни объекта, например при выходе из функции. Итак, задача создания и уничтожения объектов решена. Если конструктор и деструктор соответствуют другу, память, временно занятая объектом, будет возвращена программе деструктором. • Но при этом появляется новая трудность: программа при использовании определения класса с деструктором аварийно завершается. Какую же недопустимую операцию она выполняет? • Чтобы понять природу катастрофы, нужно еще раз представить себе, как передаются аргументы функции. Внутри функции foo(matrix m){. . . } никакие объекты не создаются, функция просто принимает в качестве аргумента объект типа matrix. Этот объект передается по значению, то есть функция получает не сам объект, а его копию, создаваемую специальным конструктором копирования. А поскольку в определении нашего класса такого конструктора нет, вызывается конструктор копирования по умолчанию, который создает полный аналог нашего аргумента. В объекте, создаваемом конструктором копирования, - две целочисленные переменные nrows и ncols, а также указатель m_, содержащий адрес памяти, где расположен двухмерный массив. •
• С копией объекта внутри функции foo() можно делать что угодно: изменять массив или выводить на экран значения отдельных элементов, но раз объект (копия аргумента) создан, перед выходом из функции должен вызываться деструктор, который мы уже написали - себе на погибель. • Ведь в деструкторе есть инструкция delete [] m_, значит, он освободит память, занимаемую нашим объектом, ведь именно на нее указывает m_! To есть при выходе из функции двухмерный массив перестанет существовать, и попытки записать что-то в область памяти, которой программа уже не обладает, приводят к ее аварийному завершению. • Что же делать? Очевидно, деструктор трогать нельзя, ведь он помогает уничтожать объекты, явно созданные внутри функции. Значит, придется самим написать настоящий конструктор копирования, который выделяет память для копии объекта и переписывает туда все значения массива. Тогда при выходе из функции снова будет вызван деструктор, но теперь он уничтожит не исходный объект, а лишь его копию, занимающую совершенно другую область памяти.
• Написать конструктор копирования довольно легко, если догадаться, что объект, копия которого должна быть создана, передается по ссылке. Действительно, по значению его передать никак нельзя, ведь тогда для создания копии объекта, с которой будет работать конструктор копирования, придется вызывать конструктор копирования - получится бесконечный вызов функции без всякой надежды вернуться в основную программу. А при передаче по ссылке копия объекта не создается, что и требуется. • Конструктор копирования для класса matrix может выглядеть так: matrix(matrix &mc) { nrows_ = mc. rget(); ncols_ = mc. cget(); m_ = new double[nrows_ * ncols_]; for (int i = 0; i < nrows_; i++) for (int j = 0; j < ncols_; j++) put(i, j, mc. get(i, j)); } • В этом конструкторе сначала выясняются размеры копируемого объекта, затем оператор new выделяет новую область памяти, куда и переписываются элементы исходного двухмерного массива mc.
• Снабдив объект обычным конструктором, конструктором копирования, а также деструктором, мы решаем основные проблемы его использования и передачи функциям. Правда, если объект не захватывает память в процессе выполнения программы, то можно задействовать конструктор для задания его начального состояния, доверив компилятору вызовы деструктора и конструктора копирования, когда это требуется. • Но во всем полагаться на компилятор нельзя, потому что не всегда можно правильно понять его логику. Например, следующая инструкция создает указатель р на объект типа matrix: matrix *p = new matrix (100, 200); • При этом вызывается конструктор класса matrix, выделяющий память для двухмерного массива. Но автоматического вызова деструктора в этом случае не происходит, потому что компилятор считает, что создает всего лишь указатель, а не двухмерный массив. Значит, при выходе из функции или там, где объект, созданный оператором new, должен исчезнуть, необходимо записать инструкцию: delete p; • Тогда память, занимаемая двухмерным массивом, освободится.
• До сих пор конструктор копирования работал у нас внутри функции, где он вызывался неявно и создавал копию объекта, передаваемого по значению. • Так же неявно конструктор копирования вызывается при возврате функцией значения в основную программу. • Кроме того, конструктор копирования используется, когда при объявлении объекта ему присваивается начальное значение типа matrix: b = a;


