базовые концепции ООП — деструкторыbaseline OOP concepts — destructors

ДеструкторыDestructors

Деструктор — зеркальная операция конструктору: гарантированно вызывается ровно один раз при уничтожении объекта, освобождая ресурсы, которые объект удерживал. В C++ (в отличие от языков со сборкой мусора) деструктор — детерминированный, предсказуемый по времени вызов, а не что-то, что может произойти «когда-нибудь» — это и есть фундамент RAII.The destructor mirrors the constructor: guaranteed to run exactly once when an object is destroyed, releasing whatever resources the object held. In C++ (unlike garbage-collected languages), a destructor is a deterministic call at a predictable moment, not something that happens "eventually" — that's the foundation of RAII.

частый вопрос на собеседованииcommon interview question

ОпределениеDefinition

Деструктор — метод с именем ~ИмяКласса(), без параметров и возвращаемого значения, вызываемый автоматически при выходе объекта из области видимости (для объектов на стеке), при явном delete (для объектов в куче) или при разрушении содержащего объекта (для полей-членов).A destructor is a method named ~ClassName(), taking no parameters and returning nothing, invoked automatically when an object goes out of scope (for stack objects), on an explicit delete (for heap objects), or when a containing object is destroyed (for member fields).

class FileHandle {
    FILE* fp;
public:
    FileHandle(const char* path) : fp(fopen(path, "r")) {}
    ~FileHandle() { if (fp) fclose(fp); }   // гарантированно закроет файл// guaranteed to close the file
};

void readConfig() {
    FileHandle f("config.txt");
    // ... что угодно, включая исключение ...// ... anything, including an exception ...
}   // ~FileHandle() вызывается здесь автоматически, даже при исключении выше// ~FileHandle() is called automatically here, even if an exception was thrown above

Виртуальный деструктор обязателен при полиморфном удаленииA virtual destructor is mandatory for polymorphic deletion

Если класс проектируется как база для наследования и объекты будут удаляться через указатель на базовый тип, деструктор базы обязан быть virtual. Без этого delete вызовет только деструктор статического типа указателя, а не деструктор реального типа объекта — деструктор производного класса и деструкторы всех его полей просто не выполнятся.If a class is designed to be a base for inheritance and objects will be deleted through a pointer to the base type, the base's destructor must be virtual. Without that, delete only calls the destructor of the pointer's static type, not the object's actual type — the derived class's destructor and its fields' destructors simply never run.

struct Base {
    ~Base() {}                 // НЕ virtual — опасно для полиморфного использования// NOT virtual — dangerous for polymorphic use
};
struct Derived : Base {
    std::vector<int> data{1, 2, 3};
    ~Derived() { std::cout << "Derived cleanup\n"; }
};

Base* p = new Derived();
delete p;   // UB: вызовется только ~Base(), ~Derived() и деструктор data пропущены — утечка// UB: only ~Base() runs, ~Derived() and data's destructor are skipped — a leak

Исправление — одно слово:The fix is one word:

struct Base {
    virtual ~Base() {}   // теперь delete через Base* вызывает правильный деструктор через vtable// now delete through a Base* calls the right destructor via the vtable
};

Практическое правило: если у класса есть хотя бы одна виртуальная функция, его деструктор почти всегда тоже должен быть виртуальным. Обратное правило тоже полезно: если класс точно не будет базой для полиморфного удаления (например, помечен final или используется только по значению), виртуальный деструктор — не бесплатен (добавляет vptr, см. страницу про виртуальные таблицы) и не нужен.The practical rule: if a class has even one virtual function, its destructor should almost always be virtual too. The converse is also useful: if a class definitely won't be a base for polymorphic deletion (say, it's marked final or only ever used by value), a virtual destructor isn't free (it adds a vptr, see the virtual tables page) and isn't needed.

Порядок разрушения — строго обратный порядку созданияDestruction order — the strict reverse of construction order

Поля класса разрушаются в порядке, обратном порядку их объявления (симметрично инициализации), а базовые классы разрушаются после полей производного класса, тоже в порядке, обратном порядку наследования. Для объектов на стеке в одной области видимости порядок разрушения — строго обратный порядку создания.A class's fields are destroyed in the reverse of their declaration order (mirroring initialization), and base classes are destroyed after the derived class's own fields, also in the reverse of the inheritance order. For stack objects in the same scope, the destruction order is the strict reverse of the creation order.

struct A { ~A() { std::cout << "~A "; } };
struct B { ~B() { std::cout << "~B "; } };

void f() {
    A a;
    B b;
}   // напечатает "~B ~A" — обратный порядок создания, не прямой// prints "~B ~A" — the reverse of creation order, not the original order

Логика та же, что у стека вызовов функций: последний созданный ресурс может зависеть от ранее созданных (например, объект, хранящий ссылку на другой), значит его нужно уничтожить первым, пока то, от чего он зависит, ещё живо.The logic mirrors a call stack: the most recently created resource might depend on earlier ones (say, an object holding a reference to another), so it must be destroyed first, while whatever it depends on is still alive.

ГраблиGotchas

Исключение из деструктора во время раскрутки стека — вызов std::terminateAn exception from a destructor during stack unwinding calls std::terminate

Если деструктор бросает исключение в момент, когда стек уже раскручивается из-за другого исключения, происходит вызов двух исключений одновременно — по стандарту это приводит к немедленному std::terminate(), без шанса на восстановление. Деструкторы по соглашению обязаны не бросать исключения (с C++11 они неявно noexcept).If a destructor throws while the stack is already unwinding due to another exception, two exceptions end up in flight simultaneously — per the standard this triggers an immediate std::terminate(), with no chance of recovery. By convention, destructors must never throw (since C++11 they're implicitly noexcept).

Объявление деструктора подавляет неявные move-операцииDeclaring a destructor suppresses the implicit move operations

Как только класс определяет собственный деструктор, компилятор перестаёт неявно генерировать конструктор перемещения и оператор присваивания перемещением — вместо них при попытке "переместить" объект молча используется копирование (если оно доступно). Подробно разобрано на странице Rule of 0/3/5.The moment a class defines its own destructor, the compiler stops implicitly generating a move constructor and move assignment operator — attempting to "move" the object silently falls back to copying instead (if that's available). Covered in detail on the Rule of 0/3/5 page.

Явный вызов деструктора без последующего размещения — почти всегда ошибкаAn explicit destructor call with no follow-up placement is almost always a bug

obj.~T(); — валидный синтаксис (нужен, например, в ручной реализации std::vector с placement new), но вызов его на обычном стековом объекте оставляет память объекта неинициализированной, а компилятор всё равно вызовет деструктор повторно при выходе объекта из области видимости — двойное разрушение, undefined behavior.obj.~T(); is valid syntax (needed, say, in a hand-rolled std::vector using placement new), but calling it on an ordinary stack object leaves the object's memory uninitialized, and the compiler still calls the destructor again when the object leaves scope — a double destruction, undefined behavior.