Ответ
Правило трёх (Rule of Three) — это принцип C++, который гласит: если классу требуется явно определённый пользовательский деструктор, конструктор копирования или копирующий оператор присваивания, то, вероятно, ему требуются все три. Это следствие необходимости корректного управления ресурсами (память, файловые дескрипторы, сокеты и т.д.).
Почему это важно? Без явного определения этих методов компилятор сгенерирует их автоматически (поверхностное копирование). Для классов, владеющих сырыми указателями, это приводит к двойному освобождению памяти или утечкам.
Пример нарушения и исправления:
// НАРУШЕНИЕ: Класс с сырым указателем и только деструктором.
class BadString {
char* data;
public:
BadString(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
~BadString() { delete[] data; } // Только деструктор!
// Проблема: компилятор сгенерирует конструктор копирования и оператор присваивания,
// которые просто скопируют указатель. Два объекта будут указывать на одну область памяти.
};
// СОБЛЮДЕНИЕ ПРАВИЛА ТРЁХ:
class String {
char* data;
public:
String(const char* str = "") {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
~String() { delete[] data; } // 1. Деструктор
// 2. Конструктор копирования (глубокое копирование)
String(const String& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
// 3. Копирующий оператор присваивания (с проверкой на самоприсваивание)
String& operator=(const String& other) {
if (this != &other) { // Защита от самоприсваивания (a = a)
delete[] data; // Освобождаем старые ресурсы
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
return *this;
}
};
Современный C++ (C++11 и новее): Правило пяти (Rule of Five) расширяет правило тремя, добавляя конструктор перемещения и перемещающий оператор присваивания для эффективной работы с rvalue-ссылками. Лучшей практикой является использование Правила нуля (Rule of Zero): проектировать классы так, чтобы управление ресурсами делегировалось умным указателям (std::unique_ptr, std::shared_ptr) и стандартным контейнерам, тогда все пять специальных функций будут сгенерированы корректно автоматически.
Ответ 18+ 🔞
А, слушай, вот тебе история про один из главных граблей C++, на которые наступают все новички. Представь себе: пишешь ты класс, там внутри сырой указатель, память выделяешь. Всё вроде работает, пока объект один. А потом начинаешь его копировать — и тут начинается ёперный театр. Программа падает с ошибками доступа к памяти, которые нихуя не понять. А всё почему? Потому что нарушил Правило трёх.
В чём суть, ёпта? Говорит оно вот что: если тебе приспичило в классе явно прописать деструктор, конструктор копирования или копирующий оператор присваивания, то, сука, с вероятностью 99.9% тебе нужны все три штуки сразу. Иначе — пиши пропало.
А нахуя оно надо? Да затем, что компилятор — он не телепат, блядь. Если ты не написал эти методы сам, он сгенерирует их за тебя. И сделает он это по-дурацки: просто скопирует все поля побитово (поверхностное копирование). И если у тебя там был указатель на кучу (new), то теперь два объекта будут тыкать в один и тот же кусок памяти. Один из них умрёт (вызовется деструктор) и освободит память. А второй останется с висячим указателем, и когда он тоже захочет умереть — бабах! — двойное освобождение, краш, и ты сидишь с вопросом «какого хуя?». Либо наоборот, память вообще никто не освободит — утечка. Короче, пиздец.
Смотри, как НЕ НАДО делать:
// Класс-распиздяй, который просит по морде.
class BadString {
char* data;
public:
BadString(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
~BadString() { delete[] data; } // О, красава, деструктор написал!
// А конструктор копирования и оператор =? Нихуя!
// Компилятор их сгенерит сам, и они просто скопируют указатель 'data'.
// Жди беды.
};
Вот ты создашь два таких объекта, один скопируешь из другого — и у тебя доверия ебать ноль к стабильности программы.
А вот как надо, по правилу трёх:
// Класс, который уже не манда с ушами.
class String {
char* data;
public:
String(const char* str = "") {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
~String() { delete[] data; } // 1. Деструктор. Чистим за собой.
// 2. Конструктор копирования. Делаем ГЛУБОКОЕ копирование.
String(const String& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
// 3. Копирующий оператор присваивания. Самое интересное.
String& operator=(const String& other) {
if (this != &other) { // Святое: проверка на самоприсваивание (a = a)
delete[] data; // Выкидываем старый хлам.
data = new char[strlen(other.data) + 1]; // Берём новый.
strcpy(data, other.data); // Копируем данные.
}
return *this; // Возвращаем себя.
}
};
Вот теперь всё чётко. Каждый объект владеет своей собственной копией строки в памяти. Умер один — освободил свою память. Живёт второй — трогает только свою. Порядок, ебать колотить.
Что там в современности? С C++11 появились перемещения, и Правило трёх выросло до Правила пяти — добавляются ещё конструктор перемещения и перемещающий оператор присваивания. Но самый крутой подход — это Правило нуля: сделай так, чтобы твоему классу не нужно было вообще писать ни одну из этих пяти специальных функций. Перепоручи всю грязную работу умным указателям (std::unique_ptr) и контейнерам из STL. Пусть они там сами разруливают, а твой класс будет простым и безопасным. Идеал, блядь.