Что такое "Правило трёх" в C++ и почему без него утечка памяти?
Что такое Правило трёх (Rule of Three)?
Правило трёх — это фундаментальный принцип C++, который гласит: если ваш класс явно определяет любой из следующих трёх специальных методов-членов, то он, скорее всего, должен определить все три:
- Деструктор (~Class()) — для освобождения ресурсов.
- Конструктор копирования (Class(const Class&)) — для корректного создания копии.
- Оператор присваивания копированием (Class& operator=(const Class&)) — для корректного копирования в уже существующий объект.
Почему это важно? Компилятор автоматически генерирует эти методы, если вы их не определили. Но автогенерация делает поверхностное копирование (shallow copy) — просто копирует значения всех полей. Для классов с динамической памятью (как ваш String) это приводит к тому, что два объекта указывают на один и тот же блок памяти. Когда один объект уничтожается, он освобождает память, а второй объект остаётся с висячим указателем (dangling pointer). Итог — double free или undefined behavior.
Исправление вашего класса: реализация Правила трёх
class String {
public:
// Конструктор
String(const char* str = "") {
size = strlen(str);
data = new char[size + 1];
strcpy(data, str);
}
// Деструктор
~String() {
delete[] data;
}
// Конструктор копирования (глубокое копирование)
String(const String& other) {
size = other.size;
data = new char[size + 1];
strcpy(data, other.data);
}
// Оператор присваивания копированием (глубокое копирование)
String& operator=(const String& other) {
if (this == &other) {
return *this; // Защита от самоприсваивания
}
// 1. Освобождаем старые ресурсы
delete[] data;
// 2. Копируем новые
size = other.size;
data = new char[size + 1];
strcpy(data, other.data);
return *this;
}
private:
char* data;
int size;
};
Теперь при копировании создаётся новый независимый массив. Каждый объект владеет своей памятью и безопасно её освобождает.
Правило пяти (Rule of Five) в C++11 и новее
С появлением семантики перемещения (move semantics) в C++11 добавили ещё два метода:
- Конструктор перемещения (Class(Class&&))
- Оператор присваивания перемещением (Class& operator=(Class&&))
Если вы определяете любой из пяти (деструктор, копирующий конструктор, копирующее присваивание, перемещающий конструктор, перемещающее присваивание), то рекомендуется определить все пять. Однако на практике, если вы следуете Правилу нуля (используете RAII-обёртки вроде std::string, std::vector, умные указатели), то компилятор сам сгенерирует корректные копирующие и перемещающие операции. Ваш класс String можно было бы написать так:
#include <string>
class String {
private:
std::string data; // RAII-обёртка
public:
String(const char* str = "") : data(str) {}
// Никаких явных деструкторов, копирований и т.д. — компилятор сделает всё правильно!
};
Это и есть Правило нуля — идеальный подход в современном C++.
<<Альтернативный подход: идиома "copy-and-swap"
В вашем примере оператор присваивания можно реализовать более элегантно и безопасно с помощью идиомы copy-and-swap. Она автоматически обеспечивает строгую гарантию безопасности исключений и избавляет от проверки на самоприсваивание.
class String {
public:
// ... конструктор, деструктор, конструктор копирования (как выше) ...
// Оператор присваивания через copy-and-swap
String& operator=(String other) { // Принимаем по значению! Создаётся копия.
swap(*this, other); // Меняем местами с копией
return *this;
}
// Дружественная функция для обмена
friend void swap(String& a, String& b) noexcept {
using std::swap;
swap(a.data, b.data);
swap(a.size, b.size);
}
};
Как это работает:
- Параметр
otherпринимается по значению — вызывается конструктор копирования (или перемещения, если передаётся временный объект). - Мы обмениваем содержимое
*thisиother. - При выходе из функции
otherуничтожается, унося с собой старые данные*this.
Этот подход часто проще и безопаснее ручного управления памятью в операторе присваивания. Он гарантирует, что если в процессе копирования возникнет исключение, исходный объект останется неизменным. Однако помните: для идиомы всё равно нужно корректно реализовать конструктор копирования и деструктор — то есть само Правило трёх.
Ответов пока нет.
Войдите чтобы оставить ответ