Для чего нужен виртуальный деструктор?

«Для чего нужен виртуальный деструктор?» — вопрос из категории ООП, который задают на 38% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Виртуальный деструктор нужен для корректного и полного разрушения объекта производного класса при его удалении через указатель на базовый класс. Без него может произойти утечка ресурсов или неопределенное поведение.

Проблема: Если деструктор базового класса не виртуальный, то цепочка вызовов деструкторов при delete через указатель на базовый класс определяется статическим типом указателя, а не динамическим типом объекта. В результате вызывается только деструктор базового класса.

Решение: Объявление деструктора базового класса как virtual гарантирует, что вызов пойдет по цепочке наследования, начиная с деструктора самого производного класса.

Практический пример с ресурсами:

#include <iostream>

// БАЗОВЫЙ КЛАСС БЕЗ виртуального деструктора (ПЛОХО)
class BaseBad {
public:
    BaseBad() { std::cout << "BaseBad constructorn"; }
    ~BaseBad() { std::cout << "BaseBad destructorn"; } // НЕ ВИРТУАЛЬНЫЙ!
};

// БАЗОВЫЙ КЛАСС С виртуальным деструктором (ХОРОШО)
class BaseGood {
public:
    BaseGood() { std::cout << "BaseGood constructorn"; }
    virtual ~BaseGood() { std::cout << "BaseGood destructorn"; } // ВИРТУАЛЬНЫЙ!
};

class Derived : public BaseBad, public BaseGood {
    int* resource;
public:
    Derived() : resource(new int(100)) {
        std::cout << "Derived constructor (allocated resource)n";
    }
    ~Derived() {
        delete resource; // Освобождение ресурса
        std::cout << "Derived destructor (freed resource)n";
    }
};

int main() {
    std::cout << "=== Deleting via BaseBad pointer (Undefined Behavior) ===n";
    BaseBad* badPtr = new Derived();
    delete badPtr; // ВЫЗОВЕТСЯ ТОЛЬКО ~BaseBad()! Ресурс не освободится.
    std::cout << "nResource leak!nn";

    std::cout << "=== Deleting via BaseGood pointer (Correct) ===n";
    BaseGood* goodPtr = new Derived();
    delete goodPtr; // Вызовется ~Derived(), затем ~BaseGood().
    std::cout << "nAll resources properly freed.n";

    return 0;
}

Вывод из примера (примерный):

=== Deleting via BaseBad pointer (Undefined Behavior) ===
BaseBad constructor
BaseGood constructor
Derived constructor (allocated resource)
BaseBad destructor                 // УТЕЧКА! Ресурс не освобожден.

Resource leak!

=== Deleting via BaseGood pointer (Correct) ===
BaseBad constructor
BaseGood constructor
Derived constructor (allocated resource)
Derived destructor (freed resource) // Ресурс освобожден.
BaseGood destructor
BaseBad destructor

All resources properly freed.

Правило: Если класс предназначен для полиморфного использования (т.е. через указатель/ссылку на базовый класс могут создаваться объекты производных классов), деструктор базового класса должен быть виртуальным. Для классов, не предназначенных для наследования, можно объявить деструктор как protected невиртуальный или использовать final в C++11.