C++中虚析构函数的调用顺序

Order of calling virtual destructors in C++

嗯,所以我一直试图通过 C++ 理解 OOP 概念,但是我无法获得虚拟析构函数的某些部分。

我写了一小段:

class A{
    int x;
public: 
    virtual void show(){
        cout << " In A\n"; 
    }
    virtual ~A(){
        cout << "~A\n";
    };
};

class B: public A{
    int y;
public: 
    virtual void show(){
        cout << " In B\n"; 
    }
    virtual ~B(){
        cout << "~B\n";
    };
};

class C: public A{
    int z;
public: 
    virtual void show(){
        cout << " In C\n"; 
    }
    virtual ~C(){
        cout << "~C\n";
    };
};
class E: public A{
    int z;
public: 
    virtual void show(){
        cout << " In E\n"; 
    }
    virtual ~E(){
        cout << "~E\n";
    };
};

class D: public B , public C , public E{
    int z1;
public: 
    virtual void show(){
        cout << " In D\n"; 
    }
    virtual ~D(){
        cout << "~D\n";
    };
};

signed main(){
    // A * a = new A();
    // B *b = new B();
    D *d = new D();
    B *b = d;
    C *c = d;
    E * e = d;
    A * a = new A();
    cout << d << "\n";
    cout << b  << "\n";
    cout  << c << "\n";
    cout << e << "\n";
    delete b;
    // a -> show();

}

在运行宁代码,我得到的结果是:

0x7f8c5e500000
0x7f8c5e500000
0x7f8c5e500018
0x7f8c5e500030
~D
~E
~A
~C
~A
~B
~A

现在三个问题:

According to the wikipedia article , virtual_table , it was referred that object c gets an address +8 bytes than that of d and b , what happens in case of e.

地址通常依赖于编译器,因此很冒险。我不会指望它们具有任何特定的价值。

When i call delete b instead of delete d , also get the same order sequence of virtual destructors , so why is the derived class destructor called

指针的类型无关紧要。底层对象是用 new D() 创建的,所以这些是被调用的析构函数。这是因为否则可能很难正确删除对象 -- 如果您有一个创建各种子类的工厂,您如何知道将其删除为哪种类型?

(这里实际发生的事情是(指向)析构函数存储在对象的 vtable 中。)

The virtual destructors are called only when i delete an object , then how are the vtable and vpointers gets deleted when the program ends ( when i run the code without the delete d the execution just stops without printing anything ).

如果你从不删除某些东西,它就永远不会被清理干净。程序结束时没有从堆中释放该内存。这是一个"memory leak"。当程序结束时,OS 一次性清理整个程序的堆(不管里面有什么)。

您的问题顺序:

(1) 是的,与指向最派生类型的指针相比,指向指向具有多重继承的派生 classes 对象的基的指针可能会改变它们的数值。原因是基础 class 是派生 class 的一部分,很像一个成员,位于偏移量处。仅对于多重继承中的第一个派生 class,此偏移量可以为 0。这就是为什么不能使用简单的 reinterpret_cast().

转换此类指针的原因

(2) b 指向一个 Eis-an A

这正是 virtual 对成员函数的意义:编译器生成的代码检查指向 运行 时间 的对象,并且调用为对象的实际类型(E)定义的函数,而不是用于访问该对象的表达式类型(B)。表达式的类型在编译时完全确定;实际完整对象的类型不是。

如果您不声明析构函数为虚函数,程序可能会像您预期的那样运行:编译器将创建仅调用为表达式类型定义的函数(对于 B)的代码,而无需任何运行-时间查找。非虚拟成员函数调用效率稍高;但是在析构函数的情况下,就像你的情况一样,当通过基本 class 表达式进行销毁时,行为是 undefined 。如果你的析构函数是 public 它应该是虚拟的,因为这种情况可能会发生。

Herb Sutter 撰写了 an article about virtual functions,包括值得一读的虚拟析构函数。

(3) 内存,包括动态分配的内存,在程序退出时被释放并再次可供现代标准操作系统用于其他用途。 (如果旧操作系统或独立实现提供动态分配,则情况可能并非如此。)然而,动态分配对象的 析构函数 不会被调用,这可能是个问题如果他们持有数据库或网络连接等资源,最好将其释放。

关于对象的地址。正如另一个答案中已经解释的那样,这取决于编译器。不过还是可以解释的。

多重继承对象的地址 (一个可能的编译器实现)

这里是一个可能的内存图,假设指向virtual的指针table是8个字节,int是4个字节。

Class D 首先有指向虚拟 table (vtbl_ptr 或 vptr)的指针然后是 class B 没有自己的 vtbl_ptr,因为它可以与 D 共享相同的 vtbl。

Classes C 和 E 必须自带嵌入式 vtbl_ptr。它将指向 D 的 vtbl(几乎......,有一个 thunk 问题需要处理但让我们忽略它,你可以阅读 thunk 在下面的链接中,但这并不影响对额外 vtbl_ptr).

的需求

每个附加基数的附加 vptr class 是必需的,所以当我们查看 C 或 E 时,vptr 的位置总是在同一位置,即在对象的顶部,无论是否它实际上是一个具体的 C 或者它是一个作为 C 持有的 D。对于 E 和任何其他不是第一个继承基数的基数 class 也是如此。

我们根据上面可能看到的地址:

D d; // sitting at some address X
B* b = &d; // same address
C* c = &d; // jumps over vtbl_ptr (8 bytes) + B without vtbl_ptr (8 bytes)
           // thus X + 16 -- or X + 10 in hexa
E* e = &d; // jumps in addition over C part including vtbl_ptr (16 bytes)
           // thus X + 32 -- or X + 20 in hexa

请注意,问题中出现的地址的数学运算可能略有不同,因为上述内容取决于编译器。 int 的大小可能不同,填充可能不同,vtbl 和 vptr 的排列方式也取决于编译器。


要了解有关对象布局和地址计算的更多信息,请参阅:

以及关于该主题的以下 SO 条目:

  • Object layout in case of virtual functions and multiple inheritance
  • Understanding virtual table in multiple inheritance