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