Копирование динамических объектов

Конструкторы инициализируют объект, то есть они создают среду, в которой работают методы класса. Иногда создание такой среды подразумевает захват ка­ких-то ресурсов – таких как файл, память, которые должны быть освобождены после использования. Некоторым классам требуется функция, которая будет га­рантированно вызвана при уничтожении объекта – это деструктор, который очищает память и освобождает ресурсы. Деструктор вызывается неявно, когда автоматическая переменная выходит из области видимости, удаляется объект, хранящийся в динамической памяти и т.д. Наиболее часто деструктор использу­ется для освобождения памяти выделенной конструктором. Пары конструктор/деструктор являются типичным механизмом в С++ для работы с объектами переменного размера.

Пример 22.

Рассмотрим модельный пример, в котором есть массив элементов типа Name. Конструктор класса Table должен выделить область динамической памя­ти для хранения элементов массива. Для освобождения памяти используется де­структор ~Table.

class Name

{ const char* s; // символьный указатель

...

};

class Table

{ Name* p; // указатель на массив

int size; // размер массива

public:

Table ( int s=15 ) // конструктор с параметром по умолчанию

{ p = new Name[ size=s ]; } // выделение динамической памяти

~Table { delete [ ] p; } // деструктор освобождения памяти

...

};

Функция, использующая класс Table:

void f ()

{ Table t1;

Table t2 = t1; // копирующая инициализация — проблема!

Table t3;

t3 = t2; // копирующее присваивание — проблема!

}

Так как t1 и t2 являются объектами класса Table, выражение t2 = t1 по умолчанию означает почленное копирование t1 в t2 (см. выше). При наличии у объекта элементов, являющихся указателями, такая интерпретация присваива­ния может вызвать неожиданный и обычно нежелательный эффект.

Указатели используются для динамического выделения памяти объектам переменного размера. Почленное копирование обычно является неправильным при копировании объектов, имеющих ресурсы,управляемые парой конструк­тор/деструктор.

В этом примере конструктор Table по умолчанию вызван дважды – по одному разу для t1 и t3. Он не вызывается для t2, так как эта переменная прои­нициирована копированием. Однако деструктор Table вызывался три раза – по одному разу для t1, t2 и t3!

По умолчанию копирование интерпретируется как почленное копирование, поэтому t1, t2 и t3 к концу функции f() будут содержать указатели на массив имен (Name* p), выделенный в динамической памяти при создании t1. Не оста­лось указателя на массив имен, выделенный при создании t3, так как он переза­писан присваиванием t3 = t2.

В результате при отсутствии автоматической сборки мусора эта память будет навсегда потеряна для программы. С другой стороны, массив, созданный для t1, будет и в t1, и в t2, и в t3, поэтому он будет трижды удаляться. Ре­зультат непредсказуем и, вероятно, приведет к катастрофе!

Можно избежать подобных аномалий, определив, что понимать под копи­рованием Table:

class Table

{ ...

Table ( const Table&); // копирующий конструктор

Table& operator = (const Table&); // копирующее присваивание

};

Программист может определить любой подходящий смысл этих операций копирования, но традиционным решением является поэлементное копирование хранимых элементов.

Например,

// копирующий конструктор:

Table :: Table (const Table& t)

{ p = new Name [ sz = t.sz ]; // выделение памяти

for (int i = 0; i < sz; i++)

p[ i ] = t.p[ i ]; // поэлементное копирование в цикле

}

// перегрузка оператора присваивания (функция-операции = ):

Table& Table :: operator = (const Table& t)

{ if ( this != &t) // проверка: не присваивание ли самому себе (t=t)!

{ delete [ ] p; // уничтожение старого массива

p = new Name [ sz = t.sz ]; // выделение новой памяти

for (int i = 0; i < sz; i++)

p[ i ] = t.p[ i ]; // поэлементное копирование

}

return *this; // возвращение скрытого указателя

}

Копирующий конструктор и копирующее присваивание значительно отли­чаются. Основная причина различия состоит в том, что копирующий конструк­тор инициализирует "чистую" (неинициализированную) память, в то время как копирующий оператор присваивания должен корректно работать с существую­щим объектом. Основная стратегия при реализации оператора присваивания проста: 1) защита от присваивания самому себе, 2) удаление старых элементов, 3) инициализация и копирование новых элементов. Как правило, все нестатические элементы должны быть скопированы.

Пример 23.

Рассмотрим работу программы с перегруженным конструктором для по­лучения трех вариантов создания новых объектов класса "строка" с динамиче­ским выделением памяти для строки. Использована перегрузка операции + для сложения двух строк (конкатенация) и операции = для копирующего присваива­ния.

#include<iostream.h>

#include<conio.h>

#include<string.h> // библиотека функций для работы со строками

class String // класс "строка"

{ char *str; // указатель на строку

int length; // длина строки в символах

public:

// конструкторы:

String(char *text); // 1- й использует существующую строку

String(int size=80); // 2- й с параметром размера строки по умолчанию

String(String& other_str); // 3- й копирования строки по ссылке

~String() // встроенный деструктор

{ delete str; } // освобождение динамической памяти

String operator + (String& arg); // функция-операции сложения двух строк

String& String::operator=(String& st); // функция-операции присваивания

int getlen(void); // прототип метода возврата длины строки

void showstr(void); // прототип метода вывода строки на экран

};

// Описание методов класса

String::String(char *text) // конструктор с указателем на готовую строку

{ length=strlen(text); // длина строки по функции из <string.h>

str=new char[length+1]; // выделение динамической памяти для строки

strcpy(str, text); // копирование строки функцией из <string.h>

}

String::String(int size) // конструктор с параметром размера строки

{ length=size; // инициализация длины строки

str=new char[length+1]; // выделение динамической памяти для строки

*str='\0'; // символ конца строки

}

String::String(String& other_str) // конструктор копирования по ссылке (&)

{ length=other_str.length; // длина другой строки по ссылке

str=new char [length + 1]; // выделение динамической памяти строке

strcpy(str, other_str.str); // копирование другой строки в str

}

String String::operator + (String& arg) // функция-операции + для строк

{ String temp(length + arg.length); // инициализация с длиной двух строк

strcpy(temp.str, str); // копирование строки str

strcat(temp.str, arg.str); // конкатенация двух строк

return temp; // возврат объекта с содержанием двух строк

}

String& String::operator=(String& st) // функция-операции присваивания

{ if (this != &st) // проверка: не присваивание ли самому себе (t=t)!

{ delete str; // уничтожение старой строки

length=st.length; // длина новой строки

str=new char[length+1]; // выделение динамической памяти новой строке

strcpy(str,st.str); // копирование новой строки

}

return *this; // возвращение скрытого указателя

}

int String::getlen(void) // функция определения длины строки

{ return (length); } // возврат длины строки

void String::showstr(void) // функция вывода строки с пробелами

{ cout<< str <<'\n'; }

void main() // главная функция

{ clrscr(); // чистка экрана

String Astring("This is the ready string."); // создание строки с текстом

cout<<"Astring: "; Astring.showstr(); // вывод строки Astring

String Bstring; // инициализация по умолчанию

cout <<"Max length Bstring= " << Bstring.getlen() <<'\n'; // длина=80

Bstring="Bstring to Cstring by reference"; // инициализация присваиванием

cout<<"Bstring: "; Bstring.showstr(); // вывод строки Bstring

String Cstring(Bstring); // копирование Bstring в Cstring по ссылке

cout<<"Cstring: "; Cstring.showstr(); // вывод строки Сstring

Astring="The quick brown fox"; // инициализация присваиванием

cout<<"Astring: "; Astring.showstr(); // вывод строки Аstring

Вstring=" jumps over Bill."; // инициализация присваиванием

cout<<"Bstring: "; Bstring.showstr(); // вывод строки Bstring

Cstring = Astring + Bstring; // сложение строк с присваиванием

cout<<"Cstring = Astring + Bstring:\n";

Cstring.showstr(); // вывод строки Сstring

// getch();

}

Результаты программы:

Astring: This is the ready string.

Max length Bstring= 80

Bstring: Bstring to Cstring by reference

Cstring: Bstring to Cstring by reference

Astring: The quick brown fox

Bstring: jumps over Bill.

Cstring = Astring + Bstring: The quick brown fox jumps over Bill.

Комментарии к программе.

Перегруженная операция + использует только один явный аргумент. Компилятор интерпретирует выражение Astring + Bstring как Astring.(operator + (Bstring)), результатом является возможность доступа к двум строковым объектам. Функция-операции использует указатель *this для слагаемых, напри­мер, первая и вторая строки в теле функции-операции + аналогичны таким:

String temp(this->length + arg.length);

strcpy(temp.str, this->str);

Действие перегруженной операции копирующего присваивания было рассмотрено выше (пример 22).

Наши рекомендации