27

首先,我理解为什么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

编译器如何知道在删除or时如何调用A's 和' 的析构函数?具体来说,内存是如何布局的(特别是它的虚函数表),以便可以调用析构函数和析构函数?BabABAB

我的教授建议内存将按如下方式排列(某种东西):

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

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

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

那么这是如何工作的呢?

4

4 回答 4

21

它有效,因为标准说它有效。

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

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

例如,如果我使用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, this 指向~A()等。

于 2013-04-04T17:39:06.650 回答
14

vtable 条目只是指向析构函数 for AB。刚刚定义了在执行析构函数之后,然后调用基类析构函数:

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

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

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

于 2013-04-04T17:40:09.700 回答
1

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

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

于 2013-04-04T17:42:42.520 回答
0

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

尽管在标题中您使用了问题词how,但您还在问题帖子中提到了为什么。人们已经给出了很好的技术答案,但原因似乎没有得到解决。

这特别是关于多重继承及其背后的原因

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

于 2015-01-12T07:46:29.957 回答