虚基类析构函数调用顺序问题?

4
C++ FAQs 20.05:
"虚基类很特殊,它们的析构函数只会在最终派生类的析构函数中被调用。"
我不太理解这与“数据成员析构函数先于基类析构函数”规则之间的关系。
虚基类有何特别之处?我无法理解上述内容的含义 :s

虚基类析构函数在派生类的析构函数之后被调用,因为它们提供了抽象类型的清理功能,这些类型预计将在子类中实现。 - Ryan J
1
这个答案可能有用,但不是重复的。 - merlin2011
4
虚拟基类的构造函数很特殊 - 它们总是在任何非虚拟基类的构造函数之前被首先调用。析构顺序始终与构造顺序相反。因此,虚拟基类的析构函数也很特殊,因为它们总是在最后被调用。 - Igor Tandetnik
2
一个虚拟基类(在virtual上强调)满足钻石模式,因为在到达主事件(最派生的)之前,多个中间派生共享一个基类的“实例”。正如最派生的构造函数必须直接调用虚拟基类的构造函数一样,在最派生的析构函数结束时也是如此。这与传统的常规(非虚拟)基类不同。听起来你可能混淆了虚拟基类和虚拟析构函数 - WhozCraig
@Igor,但是“反向构造顺序”意味着虚基类析构函数无论如何都是最后被调用的,所以我不明白有什么特别之处? - user997112
1
这就是我的意思: “特殊性” 实际上在于构造顺序。析构顺序只是从那个顺序中跟随而来的。我不确定为什么常见问题解答要强调析构函数。也许在上下文中这个短语更有意义。 - Igor Tandetnik
2个回答

4
虚基类的关键特性是它们始终在任何派生类对象中产生一个单一独特的基类子对象。这正是虚基类的特殊之处,使其与常规基类不同,后者可以产生多个子对象。
例如,在以下继承层次结构中:
struct B {};
struct M1 : B {};
struct M2 : B {};
struct D : M1, M2 {}

不存在虚继承。所有基类都使用常规继承方式继承。在这种情况下,类D将包含两个独立的B类型子对象:一个是由M1带入的,另一个是由M2带入的。

+-> D <-+
|       |
M1      M2
^       ^
|       |
B       B    <- there are two different `B`s in `D`

当销毁D时,正确销毁所有子对象的任务是微不足道的:每个类在继承层次结构中负责销毁其直接基类,而且只是其直接基类。这意味着M1的析构函数调用自己的B子对象的析构函数,M2的析构函数调用自己的B子对象的析构函数,而D的析构函数则调用其M1和M2子对象的析构函数。
在上述销毁计划中,所有子对象都被很好地销毁,包括两个类型为B的子对象。
但是,一旦我们切换到虚继承,情况就变得更加复杂。
struct B {};
struct M1 : virtual B {};
struct M2 : virtual B {};
struct D : M1, M2 {}

现在只有一个类型为 B 的子对象在 D 中。 M1M2 都可以看到并共享 相同的 类型为 B 的子对象作为它们的基础。

+-> D <-+
|       |
M1      M2
^       ^
|       |
+-- B --+    <- there is only one `B` in `D`

如果我们尝试将之前的销毁计划应用于这个层次结构,那么就会导致B子对象被销毁两次:M1调用B子对象的析构函数,而M2调用同一个B子对象的析构函数。
当然,这是完全不可接受的。每个子对象必须仅被销毁一次。
为了解决这个问题,在D的基类子对象中使用M1和M2时,明确禁止调用它们的B子对象的析构函数。调用B的析构函数的责任被分配给D的析构函数。当类D作为完整的独立对象(即作为最派生类)使用时,它知道其中只有一个B,并且知道必须仅调用B的析构函数一次。因此,类D的析构函数将为该唯一的类型为B的基类子对象调用B的析构函数。同时,M1和M2的析构函数甚至不会尝试调用B的析构函数。
这就是虚继承的工作原理。这也是你引用的规则基本上所说的。引用中所说的虚基类的析构函数最后被调用的部分,只是意味着每个类的析构函数都会先调用其直接常规基类的析构函数,然后再在必要时调用其虚基类的析构函数(可能是间接的)。在上面的例子中,D的析构函数调用M1和M2的析构函数,然后才调用B的析构函数。

3
你引用的书中整段文字描述了析构函数的顺序。通常,在类声明中,继承类的列表顺序决定了它们的构造顺序,然后以相反的顺序进行销毁。
虚基类意味着使用了虚拟继承:
struct Base {};

struct D : virtual Base {};
struct D1 : D, virtual Base {};
struct D2 : virtual Base, D {};

ASCII艺术警告:
        Base            Base
         | \             |  \
        /_\ \            |  /_\
         |   \           |    \
         D  /_\          |     D
         |   /           |    /
        /_\ /           /_\ /_\
         | /             |  /
         D1              D2

多重继承会将这个菱形结构折叠成一条线,但是,重点仍然得到了说明。对于 D1 和 D2 的继承顺序并不重要。对于它们两者来说,D 和 Base 的析构函数调用顺序都将是相同的。

只是澄清一下,顶部是基础,然后是D1和D2,最底部是D重新连接钻石吗? - user997112
不,我只是在说明两个半菱形D1和D2。对于它们中的每一个,D都在Base下面。我想说的是,尽管D1和D2声明继承的顺序不同,但它们的D和Base被销毁的顺序是相同的。 - jxh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接