Что такое «Правило трёх» в C++ и почему его нарушение приводит к утечкам памяти?
Что такое Правило трёх?
Правило трёх (Rule of Three) — это фундаментальный принцип C++, который гласит: если ваш класс явно определяет деструктор, конструктор копирования или оператор присваивания копированием, то, скорее всего, вам нужно определить все три этих метода. Причина в том, что эти три функции тесно связаны с управлением ресурсами (памятью, файлами, сетевыми соединениями). Если вы не определите их все, компилятор сгенерирует их автоматически, но эти автоматические версии будут просто копировать поля (поверхностное копирование), что приведёт к проблемам, как в вашем примере.
Почему ваша программа падает?
В вашем классе SimpleString есть деструктор, который освобождает память. Но нет конструктора копирования и оператора присваивания. Когда вы пишете SimpleString b = a;, компилятор создаёт b путём копирования всех полей из a. Поле data — это указатель. В результате b.data и a.data указывают на один и тот же блок памяти.
- Когда объект
bуничтожается, его деструктор вызываетdelete[] data— память освобождается. - Когда объект
aуничтожается, его деструктор пытается освободить ту же самую память — происходит double free (повторное освобождение), что приводит к неопределённому поведению (обычно к падению программы). - Аналогичная проблема с присваиванием
c = a: сначала указательc.dataтеряется (утечка памяти), а затем оба указывают на одну область.
Правильное решение: реализуем все три метода
Вот исправленная версия класса SimpleString, соблюдающая Правило трёх:
#include <iostream>
#include <cstring>
class SimpleString {
public:
char* data;
// Конструктор
SimpleString(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
std::cout << "Constructed: " << data << std::endl;
}
// Деструктор
~SimpleString() {
delete[] data;
std::cout << "Destructed" << std::endl;
}
// Конструктор копирования (глубокое копирование)
SimpleString(const SimpleString& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
std::cout << "Copy constructed: " << data << std::endl;
}
// Оператор присваивания копированием (глубокое копирование)
SimpleString& operator=(const SimpleString& other) {
if (this == &other) {
return *this; // Защита от самоприсваивания
}
// Освобождаем старую память
delete[] data;
// Выделяем новую и копируем
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
std::cout << "Assigned: " << data << std::endl;
return *this;
}
};
Что изменилось?
- Конструктор копирования выделяет новую память и копирует содержимое строки, а не просто копирует указатель. Теперь каждый объект владеет своей собственной памятью.
- Оператор присваивания сначала освобождает старую память (чтобы избежать утечки), затем выделяет новую и копирует данные. Также добавлена проверка на самоприсваивание (
if (this == &other)), чтобы случайно не удалить самого себя.
Современный C++ и Правило пяти
С появлением C++11 добавились семантика перемещения (move semantics). Правило трёх расширилось до Правила пяти: если вы определяете деструктор, конструктор копирования или оператор присваивания, вам также стоит определить конструктор перемещения и оператор присваивания перемещением. Но для большинства практических задач в современном C++ рекомендуется вообще избегать ручного управления памятью и использовать стандартные контейнеры (например, std::string), которые уже реализуют все эти правила корректно.
Помните: Правило трёх — это не просто формальность, а способ предотвратить утечки памяти, двойные удаления и другие трудноуловимые ошибки.
<<Альтернативный взгляд: используйте идиому «Управление ресурсами через RAII»
Вместо того чтобы вручную реализовывать все три метода, можно применить более безопасный подход — делегировать управление памятью умным указателям. Например, используйте std::unique_ptr или std::shared_ptr. Это автоматически решит проблему, так как умные указатели сами реализуют Правило трёх (и даже Правило пяти) внутри себя.
#include <iostream>
#include <memory>
#include <cstring>
class SimpleString {
public:
std::unique_ptr<char[]> data;
SimpleString(const char* str)
: data(std::make_unique<char[]>(strlen(str) + 1)) {
strcpy(data.get(), str);
std::cout << "Constructed: " << data.get() << std::endl;
}
// Конструктор копирования и оператор присваивания автоматически запрещены!
// (unique_ptr не копируется, только перемещается)
};
Важное замечание: std::unique_ptr запрещает копирование, поэтому вы не сможете написать SimpleString b = a; — компилятор выдаст ошибку. Если копирование всё же нужно, используйте std::shared_ptr, но помните о накладных расходах. Для большинства задач лучше всего просто использовать std::string — он уже реализует всё необходимое, и вам не придётся писать ни одного из этих методов. Это и есть суть современного C++: доверять стандартной библиотеке.
Таким образом, Правило трёх — это не столько набор действий, сколько предупреждение: если вы управляете ресурсами вручную, будьте готовы реализовать все три метода. Но лучший способ — вообще не управлять ресурсами вручную.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги