Menu

Деструкторы в C++: ~ИмяКласса, RAII, освобождение ресурсов

Деструктор выполняется автоматически при уничтожении объекта. Узнайте синтаксис ~ИмяКласса(), когда он срабатывает, почему освобождает ресурсы, и правило трёх/пяти.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Что такое деструктор

На предыдущей странице вы видели конструкторы — специальные функции, которые выполняются, когда объект рождается, чтобы задать его начальное состояние. Деструктор — это зеркальное отражение: специальная функция, которая выполняется, когда объект умирает, чтобы убрать за ним.

Вы объявляете его с именем класса, перед которым стоит тильда (~). Он не принимает параметров, ничего не возвращает, и у класса может быть ровно один деструктор. Вы почти никогда не вызываете его вручную — C++ вызывает его за вас в нужный момент.

Обратите внимание, что сообщение деструктора печатается после того, как main завершает тело функции, но до выхода из программы. Когда log выходит из области видимости на закрывающей скобке, C++ выполняет ~Logger() за вас.

Когда выполняются деструкторы

Точный момент зависит от того, где живёт объект:

  • Объекты на стеке (локальные) уничтожаются, когда выходят из области видимости — на закрывающей } блока.
  • Объекты в куче (созданные через new) уничтожаются, когда вы вызываете delete. Если вы забудете delete, деструктор не выполнится никогда, и вы получите утечку.

Этот пример делает разницу наглядной:

Объекты уничтожаются в обратном порядке относительно построения. a был создан первым, поэтому умирает последним. Этот порядок LIFO (last-in, first-out — последним вошёл, первым вышел) важен, когда объекты зависят друг от друга.

Почему деструкторы важны: RAII

Настоящая сила деструкторов в том, что они делают очистку автоматической и безопасной к исключениям. Вместо того чтобы помнить об освобождении ресурса на каждом пути выполнения, вы помещаете освобождение в деструктор и позволяете языку гарантировать его выполнение. Этот приём называется RAII — Resource Acquisition Is Initialization (получение ресурса есть инициализация) — и он является основой современного C++.

Здесь класс владеет буфером в куче: он выделяет память в конструкторе и освобождает в деструкторе, так что вызывающий код никогда сам не трогает new/delete.

Ключевая мысль: даже если бы исключение было выброшено после создания squares, стек был бы раскручен и ~IntArray() всё равно выполнился бы. Именно эта гарантия делает RAII таким надёжным — и поэтому в хорошем коде на C++ вы редко пишете «голый» delete.

Правило трёх (и пяти)

Класс с собственным деструктором почти всегда владеет «сырым» ресурсом, и это создаёт скрытую опасность. Сгенерированные компилятором конструктор копирования и копирующее присваивание выполняют поверхностное копирование — они копируют указатель, а не буфер, на который он указывает. Теперь два объекта хранят один и тот же указатель, и оба деструктора вызовут для него delete, что приведёт к аварии из-за двойного освобождения.

IntArray a(5);
IntArray b = a;   // поверхностное копирование: a.data и b.data — ОДИН И ТОТ ЖЕ указатель
// в конце области видимости: деструктор b освобождает буфер,
// затем деструктор a освобождает его СНОВА -> неопределённое поведение (двойное освобождение)

Это приводит к правилу трёх: если вы пишете любое из трёх — деструктор, конструктор копирования или оператор копирующего присваивания — почти наверняка вам нужны все три. В C++11 и более поздних версиях оно расширяется до правила пяти, добавляя конструктор перемещения и перемещающее присваивание.

Однако есть ещё лучшее правило — правило нуля: проектируйте классы так, чтобы вообще не управлять «сырыми» ресурсами. Храните std::vector, std::string или умный указатель, и сгенерированный компилятором деструктор сделает всё правильно бесплатно.

По умолчанию выбирайте правило нуля. Пишите собственный деструктор только тогда, когда вы действительно владеете «сырым» ресурсом, который не оборачивает за вас ни один стандартный тип.

Виртуальные деструкторы

Когда вы удаляете объект через указатель на базовый класс, деструктор должен быть virtual — иначе уничтожается только базовая часть, а производная утекает. Это одна из самых распространённых ошибок в полиморфном коде, и компилятор по умолчанию не предупреждает вас об этом.

Без virtual у ~Base вызов delete p вызвал бы только ~Base() — неопределённое поведение, а часть Derived объекта никогда не очищается. Эмпирическое правило: любому классу с виртуальными функциями (полиморфному базовому классу) нужен виртуальный деструктор. Вы точно увидите, почему это важно, как только начнёте порождать классы.

Распространённые ошибки и подводные камни

Несколько ловушек подводят почти каждого:

Несоответствие new/delete. Если вы выделяете память через new[], освобождайте через delete[]. Смешивать new[] с обычным delete (или наоборот) — неопределённое поведение.

Забытый virtual у деструктора базового класса. Как выше, удаление производного объекта через указатель на базовый класс без виртуального деструктора приводит к утечке производной части. Если вы пишете класс, предназначенный для наследования, сделайте деструктор виртуальным.

Утечка исключений из деструктора. Деструктор, который бросает исключение во время раскрутки стека, завершает вашу программу. В современном C++ деструкторы неявно noexcept — не допускайте, чтобы код очистки бросал исключения, или подавляйте исключение внутри деструктора.

Написание деструктора, который вам не нужен. Если ваши члены уже очищают себя сами, пустой ~ИмяКласса() {} лишь добавляет шум и может незаметно отключить операции перемещения. Когда очищать нечего, не пишите деструктор вообще.

Далее: наследование

Теперь вы увидели полный жизненный цикл объекта — конструкторы дают ему жизнь, деструкторы убирают за ним, а virtual-деструкторы делают эту уборку корректной, когда один класс строится на другом. Этот последний момент — предвестник следующей большой идеи: наследования, при котором класс повторно использует и расширяет данные и поведение другого. На следующей странице показано, как порождать один класс от другого, как построение и уничтожение выстраиваются в цепочку по иерархии и как соединяются части, которые вы только что изучили.

Часто задаваемые вопросы

Что такое деструктор в C++?

Деструктор — это специальная функция-член с именем ~ИмяКласса(), которая выполняется автоматически при уничтожении объекта — когда он выходит из области видимости или вы вызываете delete. Его задача — очистка: освобождение памяти, закрытие файлов или освобождение любого ресурса, которым владеет объект. Он не принимает параметров и не имеет типа возвращаемого значения, а у класса может быть только один деструктор.

Когда выполняется деструктор в C++?

Для локального объекта (на стеке) деструктор выполняется при выходе из области видимости, на закрывающей }. Для объекта в куче, созданного через new, он выполняется, когда вы вызываете delete. Члены и базовые классы уничтожаются автоматически после этого, в порядке, обратном построению.

Всегда ли нужно писать деструктор в C++?

Нет. Если ваш класс содержит только члены, которые сами себя очищают (например, std::string, std::vector или умные указатели), деструктора, сгенерированного компилятором, достаточно — не пишите свой. Собственный деструктор нужен только тогда, когда класс владеет «сырым» ресурсом, например памятью из new или открытым дескриптором файла.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ