Деструктор — это специальный метод класса в C++, который автоматически выполняется в момент уничтожения объекта и служит для освобождения захваченных им ресурсов (памяти, файлов, соединений). У каждого класса он ровно один, его имя совпадает с именем класса и начинается с тильды: ~ClassName.
Содержание
Ниже разберу, чем деструктор отличается от конструктора, как он записывается, в какие моменты вызывается, покажу минимальный рабочий пример с фактическим выводом и три частые ошибки. Язык примеров — C++, деструкторы относятся к его объектной модели (ООП).
Конструктор и деструктор: в чем разница
Конструктор и деструктор — парные методы, отвечающие за начало и конец жизни объекта. Конструктор готовит объект к работе (задает поля, захватывает ресурсы), деструктор наводит порядок перед его удалением. Их удобно держать рядом в голове по таблице.
| Признак | Конструктор | Деструктор |
|---|---|---|
| Когда вызывается | при создании объекта | при уничтожении объекта |
| Имя | ClassName |
~ClassName |
| Аргументы | могут быть | нет |
| Перегрузка | возможна, несколько вариантов | невозможна, ровно один |
| Возвращаемый тип | отсутствует | отсутствует |
| Задача | инициализация, захват ресурсов | освобождение ресурсов |
Деструктор нельзя перегрузить: у него нет параметров, разным по сигнатуре его сделать нечем. Вызывать его вручную почти никогда не нужно — компилятор расставляет вызовы сам.
Синтаксис деструктора
Деструктор объявляется внутри класса как метод с тильдой перед именем, без аргументов и без типа возврата:
class Buffer {
public:
~Buffer() {
// код освобождения ресурсов
}
};
Если деструктор не написать, компилятор сгенерирует неявный. Он корректно уничтожит поля-объекты (например, std::string или std::vector), но не тронет то, что вы захватили вручную через new, fopen или системные вызовы. Ручной ресурс — ручное освобождение в деструкторе.
Когда вызывается деструктор
Деструктор вызывается автоматически в конце жизни объекта. Основные моменты:
- объект на стеке выходит из области видимости — на закрывающей скобке блока или функции;
- объект в куче удаляется явно через
delete(для массива —delete[]); - глобальный или статический объект уничтожается при завершении программы;
- временный объект уничтожается в конце полного выражения, где он возник;
- поле-объект или элемент контейнера уничтожается вместе со своим владельцем.
Оговорка про границу (упрощенно): при вызове std::exit() локальные автоматические объекты не разматываются и их деструкторы не выполняются, а std::abort() не вызывает деструкторов вообще. Нормальный выход из области видимости — деструктор есть, аварийное завершение — деструктора может не быть.
Минимальный рабочий пример
Этот код целиком компилируется и запускается (например, g++ -std=c++17 demo.cpp -o demo). Он показывает все три частых момента вызова деструктора: выход из вложенной области видимости, delete для объекта в куче и конец main.
#include <iostream>
#include <string>
class Logger {
public:
Logger(const std::string& name) : name_(name) { // конструктор
std::cout << "Constructor: " << name_ << "\n";
}
~Logger() { // деструктор
std::cout << "Destructor: " << name_ << "\n";
}
private:
std::string name_;
};
int main() {
Logger a("A"); // на стеке, живет до конца main
{
Logger b("B"); // живет только внутри блока
} // <- здесь уничтожается b
Logger* c = new Logger("C"); // в куче
delete c; // <- здесь уничтожается c
return 0;
} // <- здесь уничтожается a
Фактический вывод в консоли:
Constructor: A
Constructor: B
Destructor: B
Constructor: C
Destructor: C
Destructor: A
Разбор по строкам. Объект a создается первым, но уничтожается последним — на закрывающей скобке main. Объект b живет только внутри вложенного блока и уничтожается на его закрывающей скобке. Объект c создан в куче через new и не удалится сам — его деструктор срабатывает ровно на delete c. В одной области видимости объекты уничтожаются в порядке, обратном созданию: a создан раньше всех и уходит последним.
Зачем нужен деструктор: освобождение ресурсов и RAII
Главная задача деструктора — гарантировать, что ресурс будет отпущен, даже если функция завершилась досрочно или выбросила исключение. На этом построена идиома RAII (Resource Acquisition Is Initialization): ресурс захватывается в конструкторе и привязан ко времени жизни объекта, а освобождается в деструкторе при его уничтожении.
Практический смысл: вы не пишете delete, fclose или unlock вручную в каждой ветке кода — за это отвечает деструктор, который вызовется автоматически. Так устроены стандартные типы: std::string и std::vector сами освобождают память, std::unique_ptr сам делает delete, std::lock_guard сам снимает мьютекс. Если поручить владение ресурсом такому типу, собственный деструктор писать не придется.
Три частые ошибки
1. Отсутствие виртуального деструктора у базового класса. Если удалять объект-наследник через указатель на базу, а деструктор базы не виртуальный — формально это неопределенное поведение, а на практике вызывается только деструктор базы, и ресурсы наследника не освобождаются.
struct Base { ~Base() { std::cout << "~Base\n"; } };
struct Derived : Base { ~Derived() { std::cout << "~Derived\n"; } };
Base* p = new Derived();
delete p; // Вывод: ~Base (~Derived НЕ вызван, ресурсы наследника утекли)
Исправление — объявить деструктор базы виртуальным. Полиморфизм — отдельная большая тема; здесь важен сам факт: базовый класс, который удаляют через указатель на базу, должен иметь virtual ~Base().
struct Base { virtual ~Base() { std::cout << "~Base\n"; } };
// Теперь delete p; даст: ~Derived, затем ~Base
2. Двойное освобождение при копировании класса с сырым указателем. Если класс владеет ресурсом через new, а копирующий конструктор не задан, копия по умолчанию скопирует сам указатель. Тогда два объекта будут указывать на один блок, и оба деструктора вызовут delete на одном адресе.
IntArray a(5);
IntArray b = a; // копия по умолчанию: b.m_array указывает на тот же массив
// в конце оба деструктора делают delete[] на один адрес
// Вывод: free(): double free detected ... -> аварийное завершение
Исправление — либо задать копирующий конструктор и оператор присваивания (упрощенно это правило трех: если классу нужен явный деструктор, ему обычно нужны еще копирующий конструктор и присваивание; с C++11 добавляют перемещающие версии — правило пяти), либо не хранить сырой указатель, а взять std::vector или std::unique_ptr.
3. Исключение, выброшенное из деструктора. С C++11 деструкторы по умолчанию помечены noexcept, поэтому выброс исключения наружу приводит к вызову std::terminate и аварийному завершению программы.
~Connection() { throw std::runtime_error("fail"); } // Вывод: terminate called ... -> abort
Исправление — не выпускать исключения из деструктора: обработать ошибку внутри (например, залогировать) и не пробрасывать ее дальше.
Выводы
- Деструктор — метод класса с именем
~ClassName, без аргументов и типа возврата; на класс он ровно один и не перегружается. - Конструктор захватывает ресурсы при создании объекта, деструктор освобождает их при уничтожении — это парные методы.
- Деструктор вызывается автоматически: при выходе объекта из области видимости, при
deleteдля объекта в куче и при завершении программы для статических объектов. - В одной области видимости объекты уничтожаются в порядке, обратном созданию.
- На автоматическом вызове деструктора построена идиома RAII: захват в конструкторе, освобождение в деструкторе, никакой ручной очистки в каждой ветке кода.
- Базовому классу с наследниками, который удаляют через указатель на базу, нужен виртуальный деструктор.
Где применяется / связь с практикой
Деструкторы и RAII — каждодневная практика C++: работа с памятью, файлами, соединениями и блокировками почти всегда завязана на время жизни объекта. Понимание того, когда вызывается деструктор, отделяет код с утечками и падениями от кода, который сам за собой убирает.
Освойте тему на практике
Разобрать это на практике, с проверкой на компиляторе и код-ревью, удобнее системно. На курсе C++ Basic деструкторы, конструкторы и управление ресурсами разбирают на живых задачах. А посмотреть формат занятий и задать вопрос преподавателю можно на бесплатных вебинарах Otus.
Смежные темы: Концепция ООП-разработки, Программирование на C++: что должен знать начинающий разработчик, Все о наследовании в Си-семействе.
FAQ
Можно ли вызвать деструктор вручную через obj.~ClassName()?
Технически можно, но почти всегда это ошибка: после ручного вызова автоматический деструктор сработает еще раз и даст двойное освобождение. Явный вызов оправдан только в связке с placement new, когда вы сами управляете памятью.
В каком порядке вызываются деструкторы при наследовании?
Сначала выполняется деструктор наследника, затем деструктор базового класса — обратно порядку вызова конструкторов (база строится первой, разрушается последней).
Нужен ли деструктор, если в классе только std::string, std::vector и умные указатели?
Нет. Эти типы освобождают свои ресурсы сами в собственных деструкторах, а компилятор вызовет их за вас. Класс без ручных ресурсов обходится неявным деструктором — это называют правилом нуля.



