В этой главе мы сосредоточим свое внимание на копировании. Это важное, но скорее техническое понятие. Что мы имеем в виду, копируя нетривиальный объект? До какой степени копии являются независимыми после выполнения операции копирования? Какие виды копирования существуют? Как их указать? Как они связаны с другими фундаментальными операциями, например с инициализацией и очисткой?
Мы обязательно обсудим проблему манипуляции памятью без помощи высокоуровневых типов, таких как 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. Конструкторы копирования