Menu
Coddy logo textTech

Перегрузка оператора присваивания

Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 44 из 104.

Оператор присваивания копированием (=) вызывается, когда вы присваиваете один существующий объект другому. В отличие от конструктора копирования (который создает новый объект), оператор присваивания работает с объектом, который уже существует и может иметь ресурсы, которые необходимо сначала очистить.

class Buffer {
    int* data;
    size_t size;
    
public:
    Buffer(size_t s) : size(s), data(new int[s]) {}
    ~Buffer() { delete[] data; }
    
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {              // 1. Проверка на самоприсваивание
            delete[] data;                  // 2. Очистка существующего ресурса
            size = other.size;              // 3. Копирование данных
            data = new int[size];
            for (size_t i = 0; i < size; ++i)
                data[i] = other.data[i];
        }
        return *this;                       // 4. Возврат *this
    }
};

Проверка на самоприсваивание (this != &other) критически важна. Без неё b = b; удалит данные перед попыткой их копирования, что приведет к неопределенному поведению.

Оператор возвращает *this по ссылке, чтобы обеспечить возможность цепочки присваиваний вида a = b = c;. Это соответствует поведению встроенных типов.

Для классов, управляющих ресурсами, вы часто будете реализовывать как копирующий, так и перемещающий операторы присваивания:

Buffer& operator=(Buffer&& other) noexcept {
    if (this != &other) {
        delete[] data;           // Очистка текущего ресурса
        data = other.data;       // Захват ресурса
        size = other.size;
        other.data = nullptr;    // Оставление источника в валидном состоянии
        other.size = 0;
    }
    return *this;
}

Оператор перемещающего присваивания передает владение вместо копирования, делая такие операции, как vec[0] = createBuffer();, гораздо более эффективными.

challenge icon

Задание

Легко

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

Вы создадите два файла для организации своего кода:

  • DynamicString.h: Определите класс DynamicString, который хранит строку в стиле C, используя динамическую память. Ваш класс должен управлять:
    • Приватным указателем char* для данных строки
    • Приватной переменной size_t для длины
    • Конструктором, который принимает const char* и создает глубокую копию
    • Деструктором, который правильно освобождает выделенную память
    • Оператором копирующего присваивания, который выполняет глубокое копирование с защитой от самоприсваивания
    • Оператором перемещающего присваивания, который эффективно передает владение
    • Методом c_str(), который возвращает внутреннюю строку (const)
    • Методом length(), который возвращает длину строки (const)

    Ваш оператор копирующего присваивания должен следовать четырем основным этапам: проверка на самоприсваивание, очистка существующих ресурсов, копирование новых данных и возврат *this. Оператор перемещающего присваивания должен «украсть» указатель источника и оставить источник в валидном пустом состоянии (nullptr с длиной 0).

  • main.cpp: Считайте три строки из входных данных (каждую с новой строки). Создайте три объекта DynamicString: str1 с первой строкой, str2 со второй и str3 с третьей.

    Продемонстрируйте работу ваших операторов присваивания:

    1. Выведите начальное состояние всех трех строк
    2. Используйте копирующее присваивание: str1 = str2;
    3. Выведите str1 и str2 после копирующего присваивания (оба должны иметь одинаковое содержимое)
    4. Используйте перемещающее присваивание: str2 = std::move(str3);
    5. Выведите str2 после перемещающего присваивания и длину str3 (она должна быть равна 0 после перемещения)

    Формат вывода:

    Initial:
    str1: <value>
    str2: <value>
    str3: <value>
    After copy (str1 = str2):
    str1: <value>
    str2: <value>
    After move (str2 = std::move(str3)):
    str2: <value>
    str3 length: 0

Используйте <cstring> для функций strlen и strcpy. Пометьте оператор перемещающего присваивания как noexcept. Включите <utility> в main.cpp для использования std::move. Не забудьте про guard-вставки (header guards) в заголовочном файле.

Шпаргалка

Оператор присваивания копированием (=) присваивает один существующий объект другому. Он должен выполнять очистку существующих ресурсов перед копированием новых данных.

Структура оператора присваивания копированием

ClassName& operator=(const ClassName& other) {
    if (this != &other) {              // 1. Проверка на самоприсваивание
        // 2. Очистка существующих ресурсов
        // 3. Копирование данных из other
    }
    return *this;                       // 4. Возврат *this
}

Пример с динамической памятью

class Buffer {
    int* data;
    size_t size;
    
public:
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            delete[] data;                  // Очистка существующего ресурса
            size = other.size;
            data = new int[size];
            for (size_t i = 0; i < size; ++i)
                data[i] = other.data[i];
        }
        return *this;
    }
};

Проверка на самоприсваивание (this != &other) предотвращает удаление данных перед их копированием в таких случаях, как b = b;.

Возврат *this по ссылке позволяет создавать цепочки присваиваний: a = b = c;.

Оператор присваивания перемещением

Оператор присваивания перемещением передает владение вместо копирования, что повышает эффективность:

ClassName& operator=(ClassName&& other) noexcept {
    if (this != &other) {
        // Очистка текущего ресурса
        // "Кража" ресурса у other
        // Оставление other в валидном состоянии (nullptr, 0 и т.д.)
    }
    return *this;
}

Пример присваивания перемещением

Buffer& operator=(Buffer&& other) noexcept {
    if (this != &other) {
        delete[] data;
        data = other.data;       // "Кража" ресурса
        size = other.size;
        other.data = nullptr;    // Оставление источника в валидном состоянии
        other.size = 0;
    }
    return *this;
}

Помечайте оператор присваивания перемещением как noexcept для оптимальной производительности со стандартными контейнерами.

Попробуйте сами

#include <iostream>
#include <string>
#include <utility>
#include "DynamicString.h"

using namespace std;

int main() {
    // Считать три строки из входного потока
    string input1, input2, input3;
    getline(cin, input1);
    getline(cin, input2);
    getline(cin, input3);

    // Создать три объекта DynamicString
    DynamicString str1(input1.c_str());
    DynamicString str2(input2.c_str());
    DynamicString str3(input3.c_str());

    // TODO: Вывести начальное состояние всех трех строк
    // Формат:
    // Initial:
    // str1: <value>
    // str2: <value>
    // str3: <value>

    // TODO: Использовать присваивание копированием: str1 = str2;

    // TODO: Вывести str1 и str2 после присваивания копированием
    // Формат:
    // After copy (str1 = str2):
    // str1: <value>
    // str2: <value>

    // TODO: Использовать присваивание перемещением: str2 = std::move(str3);

    // TODO: Вывести str2 после перемещения и длину str3
    // Формат:
    // After move (str2 = std::move(str3)):
    // str2: <value>
    // str3 length: 0

    return 0;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование