Как базовый деструктор вызывает производный деструктор?

0

В приведенном ниже коде b является указателем базового класса. Однако, когда я вызываю деструктор (явно или неявно через delete), сначала вызывается деструктор производного класса. Я не понимаю, как это работает. Могло быть любое количество производных классов, каждый со своими собственными деструкторами. Как компилятор может узнать, какой деструктор производного класса вызывается из базового деструктора?

#include <iostream>

using namespace std;

class Base {
public:
    virtual ~Base() { cout << "Base destructor" << endl; }
};

class Derived : public Base {
public:
    ~Derived() { cout << "Derived destructor" << endl; }
};

int main(int argc, char * argv[]) {

    Base * b = new Derived();
    b->~Base(); // delete b; has the same result
}
  • 2
    Так же, как обычные виртуальные функции.
  • 0
    А ну понятно. Я не осознавал, что виртуальные функции производных классов могут вызываться из указателей базовых классов. Я вижу, что деструкторы - это просто особый случай.
Теги:
oop
destructor

5 ответов

1

Это не так. Это происходит наоборот. Обычная отправка виртуальной функции вызывает производный деструктор, а производный деструктор вызывает базовый деструктор.

1

Вызов виртуального деструктора работает так же, как вызов любой другой виртуальной функции, в результате виртуальной отправки через виртуальную таблицу. Кроме того,

b->~Base(); // delete b; "has the same result"

это не так, потому что delete также освобождает память, которую вы здесь не сделали. delete b вызывает деструктор для *b и освобождает необработанную память для операционной системы. Вы только уничтожили здание, но не отступили.

  • 0
    Извините, это было неясно. Я просто имел в виду, что он имеет тот же результат с точки зрения вызова деструкторов. Я хотел убедиться, что удаление не имеет особого поведения.
1

dynamic binding, компилятор не принимает решения, среда выполнения, потому что деструктор является виртуальным. C++ разрушение вызывает деструктор текущего класса и неявно вызывает родительский класс, пока он не попадет в базовый класс.

1

Это делается так же, как и виртуальные функции. Его называют динамическим связыванием. Когда не виртуальные функции-члены решаются статически, во время компиляции виртуальные члены динамически разрешаются во время выполнения. Компилятор поддерживает vtable для этого. Если объект имеет одну или несколько виртуальных функций, компилятор помещает скрытый указатель в объект, называемый "виртуальный указатель" или "v-указатель". Этот v-указатель указывает на глобальную таблицу, называемую "виртуальная таблица" или "v-таблица". Подробнее читайте здесь.

0

Примечание. Мой первый ответ был настолько неактивным, что я удалил его. Это было так далеко, что кто-то должен был проголосовать за мой ответ. Это еще одна попытка.

На начальном посту master_latch спросил:

Как компилятор может узнать, какой деструктор производного класса вызывается из базового деструктора?

Как это происходит, это конкретная реализация.

Почему это должно произойти, "потому что стандарт говорит так". Вот что говорит стандарт:

С++ 11 12.4 пункт 5:

После выполнения тела деструктора и уничтожения любых автоматических объектов, выделенных в теле, деструктор для класса X вызывает деструкторы для прямых невариантных элементов Xs, деструкторов для прямых базовых классов Xs и, если X является типом большинства производный класс, его деструктор вызывает деструкторы для виртуальных базовых классов Xs. Все деструкторы вызываются так, как если бы они ссылались на квалифицированное имя, то есть игнорировали любые возможные виртуальные переопределяющие деструкторы в более производных классах. Базы и члены уничтожаются в обратном порядке завершения их конструктора. Оператор return в деструкторе не может напрямую вернуться к вызывающему; до передачи управления вызывающему, вызываются деструкторы для членов и оснований. Деструкторы для элементов массива вызываются в обратном порядке их построения.

С++ 11 12.4 пункт 10:

В явном вызове деструктора имя деструктора появляется как ~, за которым следует имя типа или спецификатор decltype, который обозначает тип класса деструкторов. Вызов деструктора подчиняется обычным правилам для функций-членов,...

Пример кода в С++ 11 12.4, пункт 10, указывает на намерение выше:

struct B {
  virtual ~B() { }
};
struct D : B {
  ~D() { }
};

D D_object;
B* B_ptr = &D_object;

void f() {
  D_object.B::~B();   // calls Bs destructor
  B_ptr->~B();        // calls Ds destructor
  ...
}



master_latch, ваш пример использования b->~Base(); идентичен второму вызову в примере кода. Подумайте о b->~Base(); как будто это означало b->__destruct_me(). Это в некотором смысле не отличается от вызова любой другой виртуальной функции.

Соответствующая реализация должна сделать это, потому что "потому что стандарт говорит так". Как это делает реализация? Стандарт не говорит. (Это хорошее требование, кстати. Скажи, что нужно сделать, но не говори, как это сделать.)

Большинство реализаций (каждая реализация, в которую я вступил) делают это, создавая несколько функций для деструктора. Одна функция реализует тело деструктора, как указано программистом. Деструктор обертки выполняет это тело функции деструктора, а затем уничтожает нестатические элементы данных в обратном порядке построения, а затем вызывает дескрипторы родительского класса. То, что классы могут фактически наследовать от какого-то родительского класса, добавляет еще один поворот. Это означает, что может понадобиться третья функция деструктора для данного класса.

Итак, как реализация знает, что b->~Base() должен вызывать деструктор оболочки для class Derived? Динамическое наведение указателя на полиморфный класс на указатель void * дает указатель на самый производный объект.

С++ 11 5.2.7 пункт 7:

Если T является "указателем на cv void ", тогда результат является указателем на самый производный объект, на который указывает v. В противном случае применяется проверка времени выполнения, чтобы проверить, можно ли преобразовать объект, на который указывает или ссылается v на тип, обозначенный или обозначаемый T

Другими словами, динамическое отображение полиморфного указателя на void* дает указатель на объект по мере его объявления или выделения. Виртуальная таблица (а не часть стандарта) определяет, как найти деструктор. Реализация гарантирует, что указатель на виртуальную таблицу можно определить с помощью указателя void* на самый производный объект. Это то, что позволяет реализации знать, какой деструктор вызывает. С этого момента указатель на этот самый производный объект больше не является указателем void*.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню