Копирование объектов по умолчанию
#include <iostream>
class IntArray
{
public:
explicit IntArray(size_t size) : size(size), ptr(new int[size]) {}
void print()
{
std::cout << "Object: " << this << " array: " << ptr << "\n";
}
~IntArray()
{
delete [] ptr;
}
private:
size_t size_;
int * data_;
};
int main()
{
Создать на стеке size_ = 10 и указатель data_ на динамический массив размера 10:
IntArray a1(10);
Создать на стеке size_ = 20 и указатель data_ на динамический массив размера 20:
IntArray a2(20);
КОПИРОВАНИЕ (вместо описанного конструктора вызывается конструктор копирования (в данном случае неявно сгенерирован компилятором): size_ = 10, а указатель data_ указывает на ту же самую область памяти что и a1.data_.
IntArray a3 = a1;
ПРИСВАИВАНИЕ: a2 size_ теперь равен 10, а указатель data_ станет указывать на тот же самый динамический массив размера 10 что и a1:
a2 = a1;
return 0;
}
Какие при этом возникают проблемы:
1. При вызове деструктора у a3 будет освобождена память, выделенная для массива размера 10. А это значит что при вызове деструктора a2 и a1 произойдет ошибка времени компиляции, поскольку они попытаются освободить уже свободную память.
2. При присваивании a2 = a1 произойдёт утечка памяти, поскольку мы не освободили память, выделенную под динамический массив размера 20.
Естественно можно переопределить поведение при копировании и присваивании, для этого нужно переопределить два метода - конструктор копирования и оператор присваивания (по умолчанию эти методы сгенерированы компилятором).
Конструктор копирования
Конструктор копирования принимает один аргумент - константную ссылку на объект того же типа.
Следующий конструктор копирования вместо копирования указателя на динамический массив будет выделять новую область памяти того же размера и копировать значения массива:
struct IntArray
{
IntArray(IntArray const& a) : size_(a.size_), data_(new int[size_])
{
for (size_t i = 0; i != size_; ++i)
data_[i] = a.data_[i];
}
...
private:
size_t size_;
int * data_;
};
Конструктор копирования вызывается там, где в результате копирования создается новый экземпляр (в том числе, при передаче в функцию!). При этом если бы конструктор копирования принимал вместо ссылки сам объект, то это была бы передача по значению. Для передачи по значению происходит копирования объекта, а для копирования объекта вызывается конструктор копирования… т.е. для вызова конструктора копирования нужно вызвать конструктор копирования - рекурсия.
Оператор присваивания
Следующий оператор присваивания будет освобождать память выделенную под прошлый динамический массив и создавать новый:
struct IntArray
{
IntArray & operator=(IntArray const & a)
{
if (this != &a) // если вызвать оператор присваивания для самого себя, то ...
{
delete [] data_; // ... удалим нужный массив с концами
size_ = a.size_;
data_ = new int[size_];
for (size_t i = 0; i != size_; ++i)
data_[i] = a.data_[i];
}
return *this; // обязательно возвращает ссылку на текущий экземпляр
}
...
};
Оператор присваивания вызывается там, где копирование происходит в уже существующий объект (а значит нужно сначала удалить данные которые есть в объекте).
Странно защищать оператор присваивания от самого себя, ведь врядли кто-то напишет подобное: a1 = a1
, однако это могут сделать случайно: к примеру через указатели.
return *this
нужен для того, что бы работал такой синтаксис: a = b = c
, т.е. a присвоится то же значение что и было присвоено b.
Метод swap
Реализацию оператора присваивания можно упростить, используя метод swap (обменивает значения полей двух экзмепляров) и конструктор копирования:
struct IntArray
{
void swap(IntArray & a)
{
size_t const t1 = size_;
size_ = a.size_;
a.size_ = t1;
int * const t2 = data_;
data_ = a.data_;
a.data_ = t2;
}
// Конструктор копирования
IntArray(IntArray const& a) : size_(a.size_), data_(new int[size_])
{
for (size_t i = 0; i != size_; ++i)
data_[i] = a.data_[i];
}
// Оператор присваивания
IntArray & operator=(IntArray const& a)
{
if (this != &a)
IntArray(a).swap(*this); // аналогично:
// IntArray t(a); - временная переменная с полями как у а
// t.swap(*this); - поменять местами поля t и this
// t будет уничтожено, а в this будут значения а
return *this;
}
private:
size_t size_;
int * data_;
};
Для реализации swap можно использовать std::swap из algorithm:
#include <algorithm>
struct IntArray
{
void swap(IntArray & a)
{
std::swap(size_, a.size_);
std::swap(data_, a.data_);
}
...
private:
size_t size_;
int * data_;
};
Запрет копирования объектов
Для того что бы запретить копирование, нужно объявить конструктор копирования и оператор присваивания как private и не определять их:
struct IntArray
{
private:
IntArray(IntArray const& a);
IntArray & operator=(IntArray const& a);
}
Итог
Компилятор генерирует четыре метода:
- Конструктор без параметров
- Конструктор копирования
- Оператор присваивания
- Деструктор
Правило трёх: если потребовалось переопределить хотя бы один из: конструктор копирования, оператор присваивания, деструктор, то это значит что нужно переопределить и остальные методы из списка.