В каком порядке будут вызваны деструкторы при удалении указателя базового класса с виртуальным деструктором, указывающего на объект производного класса?

Ответ

При удалении объекта через указатель на базовый класс, если деструктор базового класса объявлен как virtual, гарантируется правильный порядок вызова деструкторов в иерархии наследования:

  1. Деструктор производного класса.
  2. Деструктор базового класса.

Почему это важно? Без виртуального деструктора вызовется только деструктор базового класса, что приведёт к неопределённому поведению (UB) и утечке ресурсов, выделенных в производном классе.

Пример на C++:

#include <iostream>

class Base {
public:
    virtual ~Base() { // Виртуальный деструктор
        std::cout << "Base destructor called.n";
    }
};

class Derived : public Base {
    int* dynamicArray;
public:
    Derived() : dynamicArray(new int[100]) {}
    ~Derived() override {
        delete[] dynamicArray; // Освобождение ресурсов производного класса
        std::cout << "Derived destructor called.n";
    }
};

int main() {
    Base* ptr = new Derived(); // Указатель базового типа на объект Derived
    delete ptr; // Благодаря virtual ~Base() вызовется цепочка деструкторов
    return 0;
}

Вывод программы:

Derived destructor called.
Base destructor called.

Таким образом, виртуальный деструктор обеспечивает корректное и полное разрушение объекта производного класса при работе с полиморфными иерархиями.

Ответ 18+ 🔞

Э, слушай, вот история про то, как накосячить можно капитально, если в базовом классе деструктор не виртуальный объявить. Представь себе: у тебя есть какой-нибудь Base — ну, типа, общая штука, а от него уже наследуется Derived, который внутри себя массив на сотню интов через new держит. Овердохуища памяти, да?

И вот ты, такой умный, создаёшь объект Derived, но хранишь указатель на него в переменной типа Base*. Потом, когда дело доходит до delete ptr;, начинается самое интересное. Если деструктор в Base — обычный, не виртуальный, то компилятор, видя тип указателя, тупо вызовет только деструктор Base. А деструктор Derived? Да похуй, он про него и не вспомнит! И что получается? Массив-то наш, выделенный в конструкторе Derived, так и остаётся висеть в памяти, никому не нужный. Утечка, ёпта! Сам от себя охуел бы, когда бы это в реальном проекте нашёл.

А теперь смотри, как надо. Объявляешь деструктор в Base как virtual ~Base(). Всё. Теперь, когда ты делаешь delete по указателю на базовый класс, компилятор понимает: «Ага, тут полиморфизм, надо проверить, что за объект на самом деле». Он лезет в таблицу виртуальных функций, видит: «Опа, а это же Derived!» И вызывает цепочку правильно: сначала ~Derived() (который массив наш освободит), потом уже ~Base(). Порядок, как в армии: от младшего к старшему.

Вот смотри на код, тут всё наглядно:

#include <iostream>

class Base {
public:
    virtual ~Base() { // Вот эта магия — виртуальный деструктор!
        std::cout << "Base destructor called.n";
    }
};

class Derived : public Base {
    int* dynamicArray;
public:
    Derived() : dynamicArray(new int[100]) {}
    ~Derived() override {
        delete[] dynamicArray; // Важный ресурс освобождаем тут
        std::cout << "Derived destructor called.n";
    }
};

int main() {
    Base* ptr = new Derived(); // Указатель базового типа, но объект — производный
    delete ptr; // И тут, благодаря virtual, вызовется ВСЁ, что надо
    return 0;
}

Запустишь — вывод будет чёткий:

Derived destructor called.
Base destructor called.

Вот и весь секрет. Волнение ебать пропадает сразу, когда знаешь это правило. Если пишешь класс, от которого будут наследоваться другие, и будет работа через указатели на базовый тип — делай деструктор виртуальным. Иначе потом будешь искать, куда у тебя память девается, и терпения ноль ебать останется очень быстро. Это не просто рекомендация, а прям жёсткая необходимость, если не хочешь пиздопроебибны с утечками и неопределённым поведением. Запомни раз и навсегда: полиморфный базовый класс → виртуальный деструктор. Хуй с горы, иначе.