Рассмотрим очень простой класс строк string:
struct string {
char* p;
int size; // размер вектора, на который указывает p
string(int sz) { p = new char[size=sz]; }
~string() { delete p; }
};
Строка - это структура данных, состоящая из вектора символов и длины этого вектора. Вектор создается конструктором и уничтожается деструктором. Однако это может привести к неприятностям.
Например:
void f()
{
string s1(10);
string s2(20);
s1 = s2;
}
будет размещать два вектора символов, а присваивание s1=s2 будет портить указатель на один из них и дублировать другой. На выходе из f() для s1 и s2 будет вызываться деструктор и уничтожать один и тот же вектор с непредсказуемо разрушительными последствиями. Решение этой проблемы состоит в том, чтобы соответствующим образом определить присваивание объектов типа string:
struct string {
char* p;
int size; // размер вектора, на который указывает p
string(int sz) { p = new char[size=sz]; }
~string() { delete p; }
void operator=(string&)
};
void string::operator=(string& a)
{
if (this == &a) return; // остерегаться s=s;
delete p;
p=new char[size=a.size];
strcpy(p,a.p);
}
Это определение string гарантирует, и что предыдущий пример будет работать как предполагалось. Однако небольшое изменение f() приведет к появлению той же проблемы в новом облике:
void f()
{
string s1(10);
s2 = s1;
}
Теперь создается только одна строка, а уничтожается две. К неинициализированному объекту определенная пользователем операция присваивания не применяется. Беглый взгляд на string::operator=() объясняет, почему было неразумно так делать: указатель p будет содержать неопределенное и совершенно случайное значение. Часто операция присваивания полагается на то, что ее аргументы инициализированы. Для такой инициализации, как здесь, это не так по определению. Следовательно, нужно определить
struct string {
char* p;
int size; // размер вектора, на который указывает p
string(int sz) { p = new char[size=sz]; }
~string() { delete p; }
void operator=(string&)
string(string&);
};
void string::string(string& a)
{
p=new char[size=a.size];
strcpy(p,a.p);
}
Для типа X инициализацию тем же типом X обрабатывает конструктор X(X&). Нельзя не подчеркнуть еще раз, что присваивание и инициализация - разные действия. Это особенно существенно при описании деструктора. Если класс X имеет конструктор, выполняющий нетривиальную работу вроде освобождения памяти, то скорее всего потребуется полный комплект функций, чтобы полностью избежать побитового копирования объектов:
class X {
// ...
X(something); // конструктор: создает объект
X(&X); // конструктор: копирует в инициализации
operator=(X&); // присваивание: чистит и копирует
~X(); // деструктор: чистит
};
Есть еще два случая, когда объект копируется: как параметр функции и как возвращаемое значение. Когда передается параметр, инициализируется неинициализированная до этого переменная - формальный параметр. Семантика идентична семантике инициализации. То же самое происходит при возврате из функции, хотя это менее очевидно. В обоих случаях будет применен X(X&), если он определен:
string g(string arg)
{
return arg;
}
main()
{
string s = "asdf";
s = g(s);
}
Ясно, что после вызова g() значение s обязано быть "asdf". Копирование значения s в параметр arg сложности не представляет: для этого надо взывать string(string&). Для взятия копии этого значения из g() требуется еще один вызов string(string&); на этот раз инициализируемой является временная переменная, которая затем присваивается s. Такие переменные, естественно, уничтожаются как положено с помощью string::~string() при первой возможности.