Когда нужно использовать виртуальные деструкторы в C++ и что будет, если их не объявить?
Краткий ответ: всегда объявляйте деструктор виртуальным, если класс предназначен для наследования
Виртуальный деструктор нужен, когда вы удаляете объект производного класса через указатель на базовый класс. Без него вызывается деструктор только по статическому типу (типу указателя), а не по динамическому (реальному типу объекта).
Почему так происходит?
В C++ вызов невиртуальных функций (включая деструкторы) разрешается на этапе компиляции по типу указателя. Когда вы пишете:
Base* ptr = new Derived();
delete ptr;
Компилятор видит, что ptr имеет тип Base*, и вызывает ~Base(). Деструктор ~Derived() просто игнорируется.
Если же деструктор объявлен как virtual, компилятор генерирует вызов через таблицу виртуальных функций (vtable). Во время выполнения программа смотрит на реальный тип объекта (в нашем случае Derived) и вызывает правильный деструктор — сначала ~Derived(), а затем автоматически ~Base().
Правильный код:
class Base {
public:
virtual ~Base() { std::cout << "Base destructor\n"; }
};
class Derived : public Base {
private:
int* data;
public:
Derived() { data = new int[100]; }
~Derived() override {
delete[] data;
std::cout << "Derived destructor\n";
}
};
int main() {
Base* ptr = new Derived();
delete ptr;
// Вывод: Derived destructor -> Base destructor
// Память освобождена!
}
Когда виртуальный деструктор ОБЯЗАТЕЛЕН?
- Когда класс предназначен для наследования и вы планируете работать с объектами через указатели/ссылки на базовый класс.
- Когда есть виртуальные функции. Если у класса есть хотя бы одна виртуальная функция, почти наверняка нужен и виртуальный деструктор.
- При использовании умных указателей —
std::unique_ptrиstd::shared_ptrправильно вызывают деструктор только если он виртуальный (иначе UB или утечка).
Когда можно НЕ использовать?
- Класс не предназначен для наследования (можно пометить
final). - Нет полиморфного использования — если вы никогда не удаляете объект через указатель на базовый класс.
- Тривиальные классы без динамической памяти — но даже в этом случае лучше привыкнуть добавлять
virtualна случай, если кто-то унаследует.
Современный C++ и best practices
В C++11 и новее:
- Используйте
overrideдля деструкторов в производных классах — это явно показывает намерение. - Если класс не предназначен для наследования, пометьте его
final— это даст компилятору подсказку для оптимизации. - Для интерфейсов (чисто абстрактных классов) часто пишут
virtual ~Interface() = default;— пустой, но виртуальный деструктор.
Запомните правило: если у класса есть виртуальная функция, деструктор должен быть виртуальным. Если класс предназначен для наследования — деструктор должен быть виртуальным. Исключения крайне редки.
<<Дополнительный нюанс: виртуальные деструкторы и умные указатели
В современном C++ вы редко пишете delete вручную, но проблема остаётся актуальной для умных указателей. Рассмотрим пример:
std::unique_ptr<Base> ptr = std::make_unique<Derived>();
// ptr автоматически удалится при выходе из области видимости
// НО: если ~Base() не виртуальный — UB или утечка!
std::unique_ptr по умолчанию вызывает delete через указатель на базовый класс. Без виртуального деструктора это — неопределённое поведение. std::shared_ptr умнее: он запоминает тип при создании, поэтому корректно вызывает деструктор даже без virtual. Но полагаться на это — плохая практика.
Альтернативное решение: CRTP и статический полиморфизм
Если вам критична производительность и вы не хотите платить за vtable, можно использовать CRTP (Curiously Recurring Template Pattern):
template <typename Derived>
class Base {
public:
~Base() {
static_cast<Derived*>(this)->cleanup();
}
};
class Derived : public Base<Derived> {
// ...
};
Но это редкий случай. Для 99% проектов используйте обычные виртуальные деструкторы — они понятны, безопасны и не создают проблем с производительностью в реальных приложениях.
Вывод: не пытайтесь экономить на virtual для деструктора в полиморфных классах. Это не оптимизация, а баг. Пусть компилятор делает свою работу.