Ответ
Виртуальный деструктор нужен для корректного и полного разрушения объекта производного класса при его удалении через указатель на базовый класс. Без него может произойти утечка ресурсов или неопределенное поведение.
Проблема:
Если деструктор базового класса не виртуальный, то цепочка вызовов деструкторов при 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
Все ресурсы освобождены. Можно жить дальше.
Итоговое правило, которое надо выжечь на жопе: Если твой класс предназначен для того, чтобы от него наследовались и удаляли объекты через указатель на базу (то есть для полиморфной хуйни), делай деструктор базового класса виртуальным. Это не обсуждается. Если же класс — такой себе, одноразовый, и наследников у него не будет, можешь оставить деструктор обычным или даже защищённым, чтобы случайно не наследоваться. Но в полиморфных иерархиях — только виртуальный, иначе будет вам хиросима.