Правило трех / пяти / нуля
Часть раздела Объектно-ориентированное программирование путешествия по 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. Перемещающее присваивание
};Правило нуля — это простейший подход: если ваш класс не управляет ресурсами напрямую, не определяйте ни одну из этих функций. Позвольте компилятору сгенерировать их или используйте умные указатели и стандартные контейнеры, которые управляют ресурсами за вас.
class Player {
std::string name; // std::string управляет своей собственной памятью
std::vector<int> scores; // std::vector управляет своими ресурсами
public:
Player(std::string n) : name(n) {}
// Деструктор, функции копирования или перемещения не требуются!
};Соблюдение этих правил предотвращает такие ошибки, как двойное удаление, утечки памяти и висячие указатели, которые возникают, когда одни операции определены, а другие отсутствуют.
Задание
ЛегкоДавайте создадим класс 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(), который возвращает сохраненный текст (возвращает пустую строку"", если данные равны 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().
Шпаргалка
Правило трех гласит, что если вы определяете любую из этих трех специальных функций-членов, вам следует определить все три:
- Деструктор
- Конструктор копирования
- Оператор присваивания копированием
Правило пяти расширяет это правило для современного 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. Присваивание перемещением
};Правило нуля гласит, что если ваш класс не управляет ресурсами напрямую, не определяйте никакие специальные функции-члены. Используйте умные указатели и стандартные контейнеры, которые управляют ресурсами автоматически:
class Player {
std::string name; // std::string управляет своей собственной памятью
std::vector<int> scores; // std::vector управляет своими ресурсами
public:
Player(std::string n) : name(n) {}
// Деструктор, функции копирования или перемещения не требуются!
};Соблюдение этих правил предотвращает такие ошибки, как двойное удаление, утечки памяти и висячие указатели.
Попробуйте сами
#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;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер