【问题标题】:Virtual destructors and deleting objects with multiple inheritance... How does it work?虚拟析构函数和删除具有多重继承的对象......它是如何工作的?
【发布时间】:2013-03-26 22:11:47
【问题描述】:

首先,我理解为什么在单继承和通过基指针删除对象方面需要virtual 析构函数。这特别是关于多重继承以及为什么这有效的原因。这个问题出现在我的一堂大学课程中,没有人(包括教授)知道为什么会这样:

#include <iostream>

struct A
{
    virtual ~A()
    {
        std::cout << "~A" << std::endl;
    }
    int memberA;
};

struct B
{
    virtual ~B()
    {
        std::cout << "~B" << std::endl;
    }
    int memberB;
};

struct AB : public A, public B
{
    virtual ~AB()
    {
        std::cout << "~AB" << std::endl;
    }
};

int main()
{
    AB* ab1 = new AB();
    AB* ab2 = new AB();

    A* a = ab1;
    B* b = ab2;

    delete a;
    delete b;
}

这个的输出是:

~AB
~B
~A
~AB
~B
~A

在删除ab 时,编译器如何知道如何调用AB 的析构函数?具体来说,AB的内存是如何布局的(尤其是虚函数表),才能调用AB的析构函数?

我的教授建议将内存布局(某种东西)如下:

    AB
+---------+              +----+
|  A VFT  | - - - - - -> | ~A |
+---------+              +----+
| memberA |
+---------+              +----+
|  B VFT  | - - - - - -> | ~B |
+---------+              +----+
| memberB |
+---------+

// I have no idea where ~AB would go...

我们都很好奇这些析构函数实际上是如何在内存中布局的,以及在ab 上调用delete 是如何导致正确调用所有析构函数的。删除基础对象在单继承中工作是有道理的(因为有一个单独的虚函数表可以使用),但显然我没有正确理解事物,因为我无法理解单继承版本并应用它到这个多重继承的例子。

那么这是如何工作的呢?

【问题讨论】:

  • 嗯,vtable 条目需要指向~AB,而不是~A~B...
  • @OliCharlesworth:啊,那肯定更有意义。
  • Microsoft C++ 文档有一个很好的虚拟与非虚拟销毁顺序示例msdn.microsoft.com/en-us/library/6t4fe76c.aspx#Anchor_2
  • 很简单,首先B 然后A 设置析构函数的指针(在它们的构造函数中),但都被AB 覆盖,所以,无论你删除哪个,你总是调用同一个析构函数!

标签: c++


【解决方案1】:

之所以有效,是因为标准说它有效。

实际上,编译器将对~A()~B() 的隐式调用插入~AB()。该机制与单继承完全相同,只是编译器要调用多个基析构函数。

我认为您的图表中混淆的主要来源是虚拟析构函数的多个单独的 vtable 条目。在实践中,将有一个条目分别指向~A()~B()~AB(),用于ABAB()

例如,如果我使用gcc 编译您的代码并检查程序集,我会在~AB() 中看到以下代码:

LEHE0:
        movq    -24(%rbp), %rax
        addq    $16, %rax
        movq    %rax, %rdi
LEHB1:
        call    __ZN1BD2Ev
LEHE1:
        movq    -24(%rbp), %rax
        movq    %rax, %rdi
LEHB2:
        call    __ZN1AD2Ev

这会调用~B(),后跟~A()

三个类的虚表如下:

; A
__ZTV1A:
        .quad   0
        .quad   __ZTI1A
        .quad   __ZN1AD1Ev
        .quad   __ZN1AD0Ev

; B
__ZTV1B:
        .quad   0
        .quad   __ZTI1B
        .quad   __ZN1BD1Ev
        .quad   __ZN1BD0Ev

; AB
__ZTV2AB:
        .quad   0
        .quad   __ZTI2AB
        .quad   __ZN2ABD1Ev
        .quad   __ZN2ABD0Ev
        .quad   -16
        .quad   __ZTI2AB
        .quad   __ZThn16_N2ABD1Ev
        .quad   __ZThn16_N2ABD0Ev

对于每个类,条目 #2 指的是该类的“完整对象析构函数”。对于A,这指向~A() 等。

【讨论】:

    【解决方案2】:

    vtable 条目只是指向AB 的析构函数。只是定义了一个析构函数执行后,然后调用基类析构函数:

    在执行析构函数的主体并销毁主体内分配的任何自动对象后,X 类的析构函数调用 [...] X 的直接基类和 [...] 的析构函数。

    因此,当编译器看到delete a;,然后看到A 的析构函数是虚函数时,它会使用vtable 查找a 的动态类型(即AB)的析构函数。这会找到~AB 并执行它。这会导致调用~A~B

    这不是说“调用~AB,然后是~A,然后是~B”的vtable;它只是说“调用~AB涉及调用~A~B

    【讨论】:

      【解决方案3】:

      析构函数按照“从最基础到最基础”的顺序被调用,并且与声明的顺序相反。所以首先调用~AB,然后是~B,然后是~A,因为AB是派生最多的类。

      在实际释放内存之前调用所有析构函数。虚拟函数指针的确切存储方式是一个实现细节,实际上是您不应该关心的事情。具有多重继承的类很可能包含两个指向其派生类的 VTABLES 的指针,但只要编译器和运行时库一起“按预期工作”,则完全取决于编译器 + 运行时库做他们想做的事来解决这些问题。

      【讨论】:

      • "真的是你不应该担心的事情" 哦,来吧,知道这很有趣! :) (即使它因实施而异)
      【解决方案4】:

      (我知道这个问题已经有将近两年的历史了,但在我遇到它后我忍不住提出了一个观点)

      虽然您在标题中使用了问题词如何,但您也在问题帖子中提到了why。人们已经给出了很好的技术答案,但原因似乎没有得到解决。

      这特别是关于多重继承及其背后的原因 为什么会这样

      这纯粹是猜测,但对我来说听起来很合理。最简单的看待它的方法是使用多重继承的对象由许多基础对象组成。选择性地破坏基础对象将在复合对象中留下一个,并且在处理针对复合对象的这些部分的方法时会导致不必要的复杂性。想象一下,如果你确实使用组合而不是多重继承,你会怎么做。所以最好遍历对象布局并将其作为一个整体销毁。

      【讨论】:

        猜你喜欢
        • 2011-01-12
        • 1970-01-01
        • 1970-01-01
        • 2016-05-16
        • 2016-08-14
        • 1970-01-01
        • 2012-10-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多