Menu
CoddyTech

Правило трех / пяти / нуля

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

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

Правило трёх гласит: если ты определяешь любой из этих трёх элементов, нужно определить и все три:

  • Деструктор
  • Конструктор копирования
  • Оператор присваивания копированием

Правило пяти расширяет это правило для современного C++, добавляя операции перемещения:

  • Деструктор
  • Конструктор копирования
  • Оператор присваивания копированием
  • Конструктор перемещения
  • Оператор присваивания перемещением
class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : size(s), data(new int[s]) {}
    
    ~Buffer() { delete[] data; }                          // 1. Деструктор
    Buffer(const Buffer& other);                          // 2. Конструктор копирования
    Buffer& operator=(const Buffer& other);               // 3. Присваивание копированием
    Buffer(Buffer&& other) noexcept;                      // 4. Конструктор перемещения
    Buffer& operator=(Buffer&& other) noexcept;           // 5. Присваивание перемещением
};

Оба оператора присваивания следуют одному шаблону: защищаются от самоприсваивания, освобождают старый ресурс, принимают новый и возвращают *this:

Buffer& Buffer::operator=(const Buffer& other) {   // копирующее присваивание
    if (this != &other) {
        delete[] data;
        size = other.size;
        data = new int[size];
        std::copy(other.data, other.data + size, data);
    }
    return *this;
}

Buffer& Buffer::operator=(Buffer&& other) noexcept {   // перемещающее присваивание
    if (this != &other) {
        delete[] data;
        data = other.data;
        size = other.size;
        other.data = nullptr;
        other.size = 0;
    }
    return *this;
}

Правило нуля — самый простой подход: если ваш класс напрямую не управляет ресурсами, не определяйте ни одну из этих функций. Позвольте компилятору сгенерировать их или используйте умные указатели и стандартные контейнеры, которые управляют ресурсами за вас.

class Player {
    std::string name;              // std::string самостоятельно управляет своей памятью
    std::vector<int> scores;       // std::vector самостоятельно управляет своими ресурсами
public:
    Player(std::string n) : name(n) {}
    // Деструктор, функции копирования и перемещения не нужны!
};

Соблюдение этих правил предотвращает такие ошибки, как двойное удаление, утечки памяти и висячие указатели, которые возникают, когда некоторые операции определены, а другие отсутствуют.

challenge icon

Задание

Легко

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

Создай два файла, чтобы организовать код:

  • TextBuffer.h: Определи класс TextBuffer, который хранит текст в динамически выделенном символьном массиве. Тебе понадобятся:
    • Приватные поля: указатель char* с именем data для текстового содержимого и size_t length для длины строки (без завершающего нулевого символа)
    • Параметризованный конструктор, который принимает C-строку (const char*), выделяет память, копирует содержимое и выводит "TextBuffer created: <text>"
    • Деструктор, который освобождает память (если она не равна null) и выводит "TextBuffer destroyed"
    • Конструктор копирования, который выполняет глубокое копирование и выводит "TextBuffer copied"
    • Оператор копирующего присваивания, который обрабатывает самоприсваивание, освобождает имеющиеся данные, выполняет глубокое копирование и выводит "TextBuffer copy-assigned". Возвращай *this
    • Конструктор перемещения (с пометкой noexcept), который передаёт владение и выводит "TextBuffer moved". Оставь исходный объект в допустимом пустом состоянии
    • Оператор перемещающего присваивания (с пометкой noexcept), который обрабатывает самоприсваивание, освобождает имеющиеся данные, передаёт владение и выводит "TextBuffer move-assigned". Возвращай *this
    • Метод getText(), который возвращает сохранённый текст (возвращай пустую строку "", если data равен null)
    • Метод getLength(), который возвращает длину
  • main.cpp: Продемонстрируй работу всех пяти специальных функций-членов. Считай текстовую строку из ввода, затем:
    • Создай TextBuffer с именем original, содержащий введённый текст
    • Создай copied с помощью конструктора копирования из original
    • Создай another с текстом "Temporary"
    • Используй копирующее присваивание: another = original
    • Создай moved, вызвав конструктор перемещения для original с помощью std::move()
    • Создай target с текстом "Target"
    • Используй перемещающее присваивание: target = std::move(copied)
    • Выведи "--- Final State ---"
    • Для каждого буфера выведи "original: <text> (length: <len>)": original, copied, moved, another, target

После перемещения исходные объекты (original и copied) должны содержать пустой текст и иметь длину 0, а объекты-получатели — содержать переданные данные. Так на практике демонстрируется Правило пяти: все пять функций работают вместе, обеспечивая безопасное управление ресурсами.

Подключи <cstring> для строковых функций, таких как strlen и strcpy, и <utility> для std::move().

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

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

using namespace std;

int main() {
    string input;
    getline(cin, input);

    // TODO: Создайте TextBuffer с именем 'original' и входным текстом

    // TODO: Создайте 'copied' из 'original' с помощью конструктора копирования

    // TODO: Создайте 'another' с текстом "Temporary"

    // TODO: Используйте копирующее присваивание: another = original

    // TODO: Создайте 'moved' из 'original' с помощью конструктора перемещения и std::move()

    // TODO: Создайте 'target' с текстом "Target"

    // TODO: Используйте перемещающее присваивание: target = std::move(copied)

    // TODO: Выведите "--- Final State ---"

    // TODO: Выведите состояние каждого буфера в следующем формате:
    // "original: <text> (length: <len>)"
    // Выведите для: original, copied, moved, another, target

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

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

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

Потренируйтесь самостоятельно: Онлайн-компилятор C++