Skip to the content.

Копирование объектов по умолчанию

#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);
}

Итог

Компилятор генерирует четыре метода:

Правило трёх: если потребовалось переопределить хотя бы один из: конструктор копирования, оператор присваивания, деструктор, то это значит что нужно переопределить и остальные методы из списка.