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

时间:2019-12-16 06:23:48

标签: c++ oop destructor virtual-inheritance virtual-destructor

好吧,我一直试图通过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

现在有三个问题:

  • 根据Wikipedia文章virtual_table的说法,对象c的地址比d和b的地址高8个字节,在e情况下会发生这种情况。
  • 当我调用delete b而不是delete d时,也会获得相同顺序的虚拟析构函数,所以为什么派生类析构函数称为
  • 仅当我删除对象时才调用虚拟析构函数,然后在程序结束时删除vtable和vpointers(当我在没有delete d的情况下运行代码时,执行只是停止而没有打印任何内容)。

3 个答案:

答案 0 :(得分:2)

b指向is-an E的{​​{1}}。

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

如果您未声明虚拟的析构函数,则程序可能会按预期运行:编译器将创建代码,该代码仅调用针对表达式类型(对于B)定义的函数,而无需运行-时间查询。非虚拟成员函数的调用效率更高。但是对于像您这样的析构函数,通过基类表达式进行销毁时,行为是未定义。如果您的析构函数是公共的,则应该是虚拟的,因为这种情况可能会发生。

Herb Sutter编写了an article about virtual functions,其中包括值得阅读的虚拟析构函数。

答案 1 :(得分:0)

  

根据Wikipedia文章virtual_table,对象c的地址比d和b的地址多8个字节,这在e情况下会发生。

地址通常与编译器相关,因此非常容易。我不会依赖于它们具有任何特定的价值。

  

当我调用delete b而不是delete d时,也会得到相同顺序的虚拟析构函数,所以为什么派生类析构函数称为

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

  

仅当我删除对象时,才调用虚拟析构函数,然后在程序结束时删除vtable和vpointers(当我运行不带delete d的代码时,执行就停止了,而没有打印任何内容)。

如果您从不删除任何内容,则永远不会对其进行清理。程序结束时没有从堆中释放该内存。这是“内存泄漏”。程序结束后,操作系统将一次性清理整个程序堆(无需关心其中的内容)。

答案 2 :(得分:0)

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

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

这里是一个可能的内存图,假设指向虚拟表的指针是8个字节,而int是4个字节。

enter image description here

类D 首先具有指向虚拟表(vtbl_ptr或vptr)的指针,然后进入类B 而没有自己的vtbl_ptr,因为它可以与D共享相同的vtbl。

C类和E类必须带有自己的嵌入式vtbl_ptr。它将指向D的vtbl(几乎...,有一个 thunk 问题要处理,但是让我们忽略它,您可以在下面的链接中阅读有关 thunk 的信息,但这不会影响对其他vtbl_ptr的需求。

每个附加基类都需要附加vptr,因此当我们查看C或E时,vptr的位置始终位于同一位置,即在对象的顶部,无论它实际上是否是具体的C还是D保留为C。E和其他不是第一个继承基的基类也是如此。

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

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条目: