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

Ответ

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

Проблема: Если деструктор базового класса не виртуальный, то цепочка вызовов деструкторов при 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.

Ответ 18+ 🔞

А, ну это классика, ёпта! Сейчас объясню на пальцах, без всей этой заумной хуйни.

Виртуальный деструктор — это такая штука, которая нужна, чтобы когда ты ебёшь мозги с полиморфизмом, всё разрушалось как надо, а не как попало. Без него получается пиздец ручной работы, утечка ресурсов и неопределённое поведение, которое потом ищи-свищи.

В чём, блядь, проблема? Смотри. Если у тебя есть базовый класс, и ты через указатель на него удаляешь объект производного класса, а деструктор базового — не виртуальный, то компилятор тупо вызовет только деструктор базового класса. А деструктор твоего наследника, где, возможно, освобождаются файлы, память или ещё какая хрень, — нихуя не вызовется. Объект разрушится наполовину, как мартышлюшка, которая открутила себе одну лапу. Это называется утечка ресурсов, и это пиздец как плохо.

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

Вот тебе наглядный пример, чтобы совсем охуеть от наглядности:

#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 << "=== Удаляем через указатель BaseBad (ПИЗДЕЦ И УТЕЧКА) ===n";
    BaseBad* badPtr = new Derived();
    delete badPtr; // ВЫЗОВЕТСЯ ТОЛЬКО ~BaseBad()! Ресурс так и останется висеть в памяти.
    std::cout << "nРесурс утёк! Удивление пиздец!nn";

    std::cout << "=== Удаляем через указатель BaseGood (ВСЁ ПУТЁМ) ===n";
    BaseGood* goodPtr = new Derived();
    delete goodPtr; // А вот тут вызовется сначала ~Derived(), потом ~BaseGood().
    std::cout << "nВсе ресурсы освобождены. Можно жить дальше.n";

    return 0;
}

Что примерно выведет эта программа:

=== Удаляем через указатель BaseBad (ПИЗДЕЦ И УТЕЧКА) ===
BaseBad constructor
BaseGood constructor
Derived constructor (allocated resource)
BaseBad destructor                 // ЁБАААТЬ! Ресурс не освободили! Утечка!

Ресурс утёк! Удивление пиздец!

=== Удаляем через указатель BaseGood (ВСЁ ПУТЁМ) ===
BaseBad constructor
BaseGood constructor
Derived constructor (allocated resource)
Derived destructor (freed resource) // Вот, память освободили, молодцы.
BaseGood destructor
BaseBad destructor

Все ресурсы освобождены. Можно жить дальше.

Итоговое правило, которое надо выжечь на жопе: Если твой класс предназначен для того, чтобы от него наследовались и удаляли объекты через указатель на базу (то есть для полиморфной хуйни), делай деструктор базового класса виртуальным. Это не обсуждается. Если же класс — такой себе, одноразовый, и наследников у него не будет, можешь оставить деструктор обычным или даже защищённым, чтобы случайно не наследоваться. Но в полиморфных иерархиях — только виртуальный, иначе будет вам хиросима.