В этой главе мы сосредоточим свое внимание на копировании. Это важное, но скорее техническое понятие. Что мы имеем в виду, копируя нетривиальный объект? До какой степени копии являются независимыми после выполнения операции копирования? Какие виды копирования существуют? Как их указать? Как они связаны с другими фундаментальными операциями, например с инициализацией и очисткой?
Мы обязательно обсудим проблему манипуляции памятью без помощи высокоуровневых типов, таких как vector и string, изучим массивы и указатели, их взаимосвязь и способы применения, а также ловушки, связанные с их использованием. Это важная информация для любого программиста, вынужденного работать с низкоуровневыми кодами, написанными на языке C++ или C.
Отметим, что детали класса vector характерны не только для векторов, но и для других высокоуровневых типов, которые создаются из низкоуровневых. Однако каждый высокоуровневый тип (string, vector, list, map и др.) в любом языке создается из одинаковых машинных примитивов и отражает разнообразие решений фундаментальных проблем, описанных в этой главе.
18.2. Копирование
Рассмотрим класс vector в том виде, в каком он был представлен в конце главы 17.
class vector {
int sz; // размер
double* elem; // указатель на элементы
public:
vector(int s) // конструктор
:sz(s), elem(new double[s]) { /* */ } // выделяет
// память
~vector() // деструктор
{ delete[ ] elem; } // освобождает
// память
// ...
};
Попробуем скопировать один из таких векторов.
void f(int n)
{
vector v(3); // определяем вектор из трех элементов
v.set(2,2.2); // устанавливаем v[2] равным 2.2
vector v2 = v; // что здесь происходит?
// ...
}
Теоретически объект v2 должен стать копией объекта v (т.е. оператор = создает копии); иначе говоря, для всех i в диапазоне [0:v.size()] должны выполняться условия v2.size()==v.size() и v2[i]==v[i]. Более того, при выходе из функции f() вся память возвращается в свободный пул. Именно это (разумеется) делает класс vector из стандартной библиотеки, но не наш слишком простой класс vector. Наша цель — улучшить наш класс vector, чтобы правильно решать такие задачи, но сначала попытаемся понять, как на самом деле работает наша текущая версия. Что именно она делает неправильно, как и почему? Поняв это, мы сможем устранить проблему. Еще более важно то, что мы можем распознать аналогичные проблемы, которые могут возникнуть в других ситуациях.
По умолчанию копирование относительно класса означает “скопировать все данные-члены”. Это часто имеет смысл. Например, мы копируем объект класса Point, копируя его координаты. Однако при копировании членов класса, являющихся указателями, возникают проблемы. В частности, для векторов в нашем примере выполняются условия v.sz==v2.sz и v.elem==v2.elem, так что наши векторы выглядят следующим образом:
Иначе говоря, объект v2 не содержит копии элементов объекта v; он ими владеет совместно с объектом v. Мы могли бы написать следующий код:
v.set(1,99); // устанавливаем v[1] равным 99
v2.set(0,88); // устанавливаем v2[0] равным 88
cout << v.get(0) << ' ' << v2.get(1);
В результате мы получили бы вектор 88 99. Это не то, к чему мы стремились. Если бы не существовало скрытой связи между объектами v и v2, то результат был бы равен 0 0, поскольку мы не записывали никаких значений в ячейку v[0] или v2[1]. Вы могли бы возразить, что такое поведение является интересным, аккуратным или иногда полезным, но мы не этого ждали, и это не то, что реализовано в стандартном классе vector. Кроме того, когда мы вернем результат из функции f(), произойдет явная катастрофа. При этом неявно будут вызваны деструкторы объектов v и v2; деструктор объекта v освободит использованную память с помощью инструкции
delete[] elem;
И то же самое сделает деструктор объекта v2. Поскольку в обоих объектах, v и v2, указатель elem ссылается на одну ту же ячейку памяти, эта память будет освобождена дважды, что может привести к катастрофическим результатам (см. раздел 17.4.6).
18.2.1. Конструкторы копирования