【问题标题】:How does the base destructor invoke the derived destructor?基析构函数如何调用派生析构函数?
【发布时间】:2014-05-17 12:09:00
【问题描述】:

在下面的代码中,b 是一个基类指针。但是,当我调用析构函数(通过删除显式或隐式调用)时,首先调用派生类析构函数。我不明白这是如何工作的。可以有任意数量的派生类,每个类都有自己的析构函数。编译器如何知道从基析构函数调用哪个派生类析构函数?

#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
}

【问题讨论】:

  • 和普通虚函数一样。
  • 哦,我明白了。我没有意识到派生类的虚函数可以从基类指针中调用。我看到析构函数只是一个特例。

标签: c++ oop destructor


【解决方案1】:

dynamic binding,编译器不决定,运行时决定,因为析构函数是虚拟的。 C++析构调用当前类的析构函数,隐式调用父类,直到碰到基类。

【讨论】:

    【解决方案2】:

    对虚拟析构函数的调用与对任何其他虚拟函数的调用相同,这是通过虚拟表进行虚拟调度的结果。除此之外,

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

    这不是真的,因为delete 也会释放内存,而您在这里没有这样做。 delete b 为*b 调用析构函数并将原始内存释放给操作系统。你只是摧毁了建筑,却没有退还土地。

    【讨论】:

    • 抱歉,不清楚。我只是说它在调用析构函数方面具有相同的结果。我想确保删除没有任何特殊行为。
    【解决方案3】:

    这与虚函数的实现方式相同。它称为动态绑定。当非虚拟成员函数在编译时静态解析时,虚拟成员在运行时动态解析。编译器为此维护了一个 vtable。如果对象具有一个或多个虚函数,编译器会在对象中放置一个隐藏指针,称为“虚指针”或“V 指针”。这个 v-pointer 指向一个名为“virtual-table”或“v-table”的全局表。从here阅读更多详细信息。

    【讨论】:

      【解决方案4】:

      它没有。它反过来发生。普通虚函数调度调用派生析构函数,派生析构函数调用基析构函数。

      【讨论】:

        【解决方案5】:

        注意:我的第一个答案太离谱了,所以我删除了它。这太离谱了,以至于有人应该对我的回答投反对票。这是另一个尝试。

        在开场白中,master_latch 问道:

        编译器如何知道从基析构函数调用哪个派生类析构函数?

        这是如何发生的取决于具体实现。

        为什么会发生这种情况是“因为标准是这样说的”。标准是这样说的:

        C++11 12.4 第 5 段:

        在执行析构函数的主体并销毁主体内分配的任何自动对象后,类 X 的析构函数调用 X 的直接非变体成员的析构函数,X 的直接基类的析构函数,如果 X 是类型在最派生类中,它的析构函数调用 X 的虚拟基类的析构函数。所有的析构函数都被调用,就好像它们被一个限定名引用一样,也就是说,忽略更多派生类中任何可能的虚拟覆盖析构函数。基和成员按照其构造函数完成的相反顺序被销毁。析构函数中的 return 语句可能不会直接返回给调用者;在将控制权转移给调用者之前,会调用成员和基的析构函数。数组元素的析构函数按其构造的相反顺序调用。

        C++11 12.4 第 10 段:

        在显式析构函数调用中,析构函数名称显示为 ~ 后跟 type-name 或 decltype-specifier,表示析构函数的类类型。析构函数的调用遵循成员函数的一般规则,...

        C++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 B’s destructor
          B_ptr->~B();        // calls D’s destructor
          ...
        }
        



        master_latch,您使用b-&gt;~Base(); 的示例与示例代码中的第二个调用相同。将b-&gt;~Base(); 视为b-&gt;__destruct_me() 的意思。从某种意义上说,它与调用任何其他虚函数没有什么不同。

        一个合规的实现必须这样做,因为“因为标准是这样说的”。它是如何实现的?标准没有说。 (顺便说一句,这是很好的需求。说必须做什么,但不要说如何去做。)

        大多数实现(我研究过的每个实现)都是通过为析构函数生成多个函数来做到这一点的。一个函数实现了程序员指定的析构函数的主体。包装析构函数执行析构函数的这个主体,然后以相反的构造顺序销毁非静态数据成员,然后调用父类析构函数。这些类实际上可以从某些父类继承,这又增加了一个转折点。这意味着可能需要给定类的第三个析构函数。

        那么实现如何知道b-&gt;~Base() 应该为class Derived 调用包装析构函数?将指向多态类的指针动态转换为 void* 指针会生成指向最派生对象的指针。

        C++11 5.2.7 第7段:

        如果T 是“指向cv void 的指针”,则结果是指向v 指向的最衍生对象的指针。否则,将应用运行时检查以查看 v 指向或引用的对象是否可以转换为 T 指向或引用的类型。

        换句话说,将多态指针动态转换为void* 会在声明或分配对象时生成指向该对象的指针。虚拟表(不是标准的一部分)指示如何找到析构函数。该实现确保指向虚拟表的指针可以从指向最派生对象的void* 指针确定。这就是让实现知道要调用哪个析构函数的原因。从那时起,指向最派生对象的指针不再是void* 指针。

        【讨论】:

          猜你喜欢
          • 2017-07-12
          • 1970-01-01
          • 2015-07-18
          • 2013-03-30
          • 1970-01-01
          • 1970-01-01
          • 2017-10-08
          • 2011-05-29
          • 2020-09-29
          相关资源
          最近更新 更多