Переопределение оператора присваивания

Последнее обновление: 12.03.2023

Компилятор по умолчанию компилирует для типов оператор присваивания, благодаря чему мы можем присваивать значения некоторого типа переменным/параметрам/константам этого типа. Создаваемый по умолчанию оператор присваивания просто копирует все переменные-члены класса одну за другой (в том порядке, в котором они объявлены в определении класса). Например:

#include <iostream>

class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    void print()  const
    {
        std::cout << "Value: " << value << std::endl;
    }
    void setValue(int val){ value=val;} // для изменения переменной value
private:
    int value;
};

int main()
{
    Counter c1{25};
    Counter c2 = c1;    // с2 получает копию состояния c1
    c1.setValue(30);    // изменения в c1 не влияют на c2
    c1.print();     // Value: 30
    c2.print();     // Value: 25
}

Здесь определен класс Counter, в котором есть переменная value. Оператор присваивания по умолчанию копирует элементы объекта справа от оператора присваивания объект того же типа слева. Когда мы присваиваем объект c1 типа Counter объекту c2, то объект c2 получает копию значению переменной value из c1:

Counter c1{25};
Counter c2 = c1;    // с2 получает копию состояния c1

Стоит отметить, что последующее изменение переменной value в одном объекте Counter, никак не затронет другой объект.

Так работает создаваемый по умолчанию оператор присваивания. Но его можно переопределить. Но стоит учитывать, что оператор присваивания можно определить только как функцию-член класса.

Оператор присваивания должен возвращать ссылку на объект, а его параметр должен представлять ссылку на константу. Реализуем подобный оператор для типа Counter:

#include <iostream>

class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    void print()  const
    {
        std::cout << "Value: " << value << std::endl;
    }
    Counter& operator=(const Counter& counter)
    {
        if(&counter != this)
        {
            value = counter.value;
        }
        return *this;
    }
private:
    int value;
};

int main()
{
    Counter c1{25};
    Counter c2{30};
    c2 = c1;    // с2 получает копию состояния c1
    c2.print();     // Value: 25
}

Посмотрим детально на реализованный оператор:

Counter& operator=(const Counter& counter)
{
    if(&counter != this)
    {
        value = counter.value;
    }
    return *this;
}

Функция оператора неконстантая, так как мы меняем в функции состояние объекта. В качестве параметра передается константная ссылка на присваиваемый объект Counter, так как его не надо изменять.

В качестве результата возвращаем ссылку объект на текущий объект Counter. Может возникнуть вопрос: почему не объект counter, который передается в функцию в качестве параметра? Дело в том, что оператор = является правоассоциативным и должен возвращать левый операнд. Например, возьмем следующую ситуацию:

Counter counter1{25};
Counter counter2{0};
Counter counter3{0};
counter3 = counter2 = counter1;

Здесь цепь присваиваний выполняется следующим образом:

counter3 = (counter2 = counter1);

То есть сначала counter2 получает значение counter1. Потому counter3 получает результат предыдущей операции, а этот результат представляет объект counter2.

Другой, на первый взгляд ненужный момент - проверка на соответствие параметра текущему объекту:

if(&counter != this)
{
    value = counter.value;
}

Если параметр представляет текущий объект, то нет смысла выполнять присваивание значения переменной value. Данная проверка должна предупредить ситуацию типа:

counter1 = counter1;

что может предупредить ненужные присваения и повысить производительность особенно в тех ситуациях, когда речь идет об освобождении/выделении памяти, и даже предпредить возможные ошибки, связанные с записью в уже особожденную память.

Копировани указателей

Иногда реализация оператора присваивания может стать вынужденной необходимостью. Например, рассмотрим следующую программу:

#include <iostream>

class Counter
{
public:
    Counter(int n)
    {
        value =new int{n};    // выделяем память
    }
    ~Counter()
    {
        delete value;    // освобождаем память
    }
    void print()  const
    {
        std::cout << *value << std::endl;
    }
private:
    int* value;
};

int main()
{
    Counter counter1{5};
    {
        Counter counter2{3};
        counter1 = counter2;
        counter1.print();     // 3
    }
    counter1.print();     // ????
}

Здесь класс Counter хранит указатель на число int. В конструкторе выделяем память для одного числа, которое передается в качестве параметра. В деструкторе особождаем память. Кажется, никаких проблем с памятью не должно возникнуть. Но возьмем применение этого класса в функции main:

Counter counter1{5};
{
    Counter counter2{3};
    counter1 = counter2;
    counter1.print();     // 3
}
counter1.print();     // ????

Во вложенном блоке создается объект counter2, который присваивается объекту counter1. Это приведет к копированию адреса указателя, в итоге объекты counter1 и counter2 будут указывать на один и тот же адрес в памяти. Но после завершения вложенного блока заканчивается область видимости объекта counter2, у него вызывается деструктор, в котором память освобождается. Соответственно освобождается и память в counter1, так как это одна и та же память. Но counter1 все еще работает, так как он создан вне вложенного блока. В итоге мы столкнемся с непредсказуемым результатом.

Чтобы выйти из этой ситуации опять же можем определить оператор присваивания:

#include <iostream>

class Counter
{
public:
    Counter(int n)
    {
        value =new int{n};    // выделяем память
    }
    ~Counter()
    {
        delete value;    // освобождаем память
    }
    void print()  const
    {
        std::cout << *value << std::endl;
    }
    Counter& operator=(const Counter& counter)
    {
        if(&counter != this)
        {
            *value = *counter.value;
        }
        return *this;
    }
private:
    int* value;
};

int main()
{
    Counter counter1{5};
    {
        Counter counter2{3};
        counter1 = counter2;
        counter1.print();     // 3
    }
    counter1.print();     // 3
}

Удаление оператора присваивания

В предыдущей ситуации есть альтернатива реализации оператора присвоения - удаление оператора присвоения по умолчанию. Для этого применяется ключевое слово delete:

Counter& operator=(const Counter& counter) = delete;

В этом случае, если в программе будет применяться операция присвоения, типа

counter1 = counter2;

То компилятор сгенерирует ошибку.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850