Теперь разберемся с копированием наших массивов. Нам необходимо явно реализовать копирующий конструктор, так как стандартный делает совсем не то, что требуется. В стандарте С++ (см. п. п. 12.8/8 в [1]) прямо сказано, что конструктор копирования, создаваемый по умолчанию, выполняет поэлементное копирование полей класса. Такое копирование называется поверхностным (shallow copying). На языке С++ реализация конструктора копирования для нашего класса ТАггау по умолчанию является такой, как показано в листинге 5.9.
Листинг 5.9. Конструктор копирования по умолчанию для класса ТАггау
ТАггау:iTArray(const ТАггау &t) { this->size_array = t.size_array; this->data = t.data;
}
Тогда следующее объявление приводит к ситуации, показанной на рис. 5.2: ТАггау А (В);
Такое поведение конструктора приводит к ошибкам типа «висячая ссылка» при уничтожении объекта. При уничтожении локальных объектов деструкторы выполняются в порядке, обратном порядку вызова конструкторов. Поэтому сначала уничтожается более «молодой» объект А. Деструктор уничтожаемого объекта возвращает память, а вторая ссылка «провисает». При уничтожении второго объекта — массива В — поведение программы непредсказуемо. Наименьшим злом будет аварийное завершение программы.
Рис. 5.2. Результат работы стандартного конструктора копирования
Нам нужно, чтобы конструктор делал совсем другое: сначала создал динамический массив такого же размера, что и копируемый, а потом скопировал в новый массив элементы инициализирующего массива. Такое копирование называется глубоким (deep copying). Текст конструктора показан в листинге 5.10.
Листинг 5.10. Конструктор копирования для класса TArray
TArray::TArray(const TArray &t)
{ this->size_array = t.size_array; // размер
this->data = new double[size_array]; // новый массив
for(Uint i = 0: i<size_array; ++i) // копируем
data[i] = t.data[i];
}
Эта ситуация принципиально отличается от предыдущей — у нас имеется отдельная копия динамического массива для каждого объекта (рис. 5.3), поэтому деструкторы сработают корректно.
С учетом того, что копируемый массив имеет заведомо корректные значения полей, для инициализации полей нового массива можно воспользоваться списком инициализации.
TArray:.TArray(const TArray &t)
: size_array(t.size_array), data(new double[size_array])
{ for(Uint i = 0: i<size_array; ++i)
data[i] = t.data[i];
Рис. 5.3. Правильная работа конструктора копирования
До сих пор мы, не задумываясь, пользовались и стандартной операцией присваивания, которая выполнятся при отсутствии явного определения операции. Прототип этой операции для некоторого типа Т выглядит так (см. п. п. 12.8/10 в [1]):
Т& operator=(const Т& );
Такая операция, как и копирующий конструктор, выполняет поэлементное копирование правого операнда в левый (см. п. п. 12.8 и 13.5.3 в [1]). Пока мы не использовали в классах динамическую память, все прекрасно работало. Однако стандартная операция присваивания для класса ТАггау работает неправильно, как и стандартный конструктор копирования. Но если стандартный конструктор провоцировал только ошибки типа «висячая ссылка», то стандартная операции присваивания создает еще и ситуацию утечки памяти — ее иллюстрирует рис. 5.4. Поэтому нам надо реализовать собственную операцию, чтобы ее работа соответствовала рис. 5.3.
Наша операция должна создать новый динамический массив, скопировать туда элементы присваиваемого массива и возвратить системе память прежнего динамического массива. Именно этим операция присваивания отличается от инициализации копированием: при инициализации нет необходимости возвращать память.
Операция присваивания, очевидно, должна возвращать ссылку на TArray, так как левый аргумент (текущий объект) стоит слева от операции и изменяется. Ссылка обеспечивает нам возможность многократного присваивания. Ставший уже классическим текст перегруженной операции присваивания для динамического класса представлен в листинге 5.11.
Листинг 5.11. «Каноническая» реализация операции присваивания TArray& TArray::operator=(const TArray &t)
{ if (this != &t) // отслеживаем самоприсваивание
{ size_array = t.size_array; // опеределили размер
double *new_data = new double[size_array]; for(Uint i = 0; i<size_array; ++i)
new_data[i] = t.data[i]; // копируем
delete[] data; // вернули предыдущий массив
data = new__data; // вступили во владение
}
return *this;
}
В этой функции надо обратить внимание на проверку операции самоприсваивания: this != &t
Это выражение позволяет нам не выполнять никакой работы, если программист напишет присваивание массива самому себе:
а = а;
Очевидно, в этом случае не требуется создавать новый динамический массив и копировать в него элементы.
Приведенный вариант реализации операции присваивания прекрасно работает и делает именно то, что нам нужно, — создает копию массива. Однако существует другое, гораздо более элегантное решение, которое показал Герб Саттер в [21]. Нам необходимо реализовать функцию обмена полей класса TArray (листинг 5.12). Аргумент у такой функции только один, так как вторым является текущий объект.
Листинг 5.12. Функция обмена с текущим объектом void Swap(TArray &other)
{ std::swap(data, other.data); // стандартная функция обмена
std: : swap(size_array, other. size__array);
}
Можно было бы написать обмен полей и явным образом: double *d = data; data = other.data; other.data = d;
Uint s = size_array; size_array = other.size_array; other.size_array = s;
Однако мы использовали функцию swap(), входящую в стандартную библиотеку, о чем сигнализирует префикс std: :.
СОВЕТ
Всегда, когда возможно, используйте средства стандартной библиотеки. |