【问题标题】:Virtual base class destructor calling ordering?虚拟基类析构函数调用排序?
【发布时间】:2014-10-02 12:23:20
【问题描述】:

C++ 常见问题解答项目 20.05:

"虚拟基类是特殊的,它们的析构函数在 最派生类的析构函数的结尾(仅)。”

我真的不明白这如何符合典型:

“先数据成员析构,后基类析构”规则

虚拟基类有什么特别之处?我不知道上面的意思是什么:s

【问题讨论】:

  • 在派生类的析构函数之后调用虚拟基类析构函数,因为它们为预期在子类中实现的抽象类型提供清理功能。
  • This answer 可能有用但不能重复。
  • 虚基构造函数是特殊的——它们总是在任何非虚基类的构造函数之前被首先调用。破坏总是以与构造相反的顺序执行。因此,虚基析构函数的特殊之处在于它们总是被最后调用。
  • 一个 virtual 基础(强调添加在 virtual 上)满足菱形模式,因为基础的单个“实例”由多个共享在到达主要事件之前的中间推导(最衍生的)。正如最派生的构造函数必须直接调用虚基的构造一样,在最派生的析构函数的结束处进行销毁也是如此。这与传统的常规(非虚拟)基础不同。听起来您对虚拟 base 和虚拟 destructor 感到困惑。
  • 这就是我要说的:“特殊性”实际上是按构造顺序排列的。破坏的顺序就从那开始。为什么FAQ选择强调析构函数,我不确定。也许这句话在上下文中更有意义。

标签: c++ polymorphism destructor


【解决方案1】:

您引用的整本书都在描述析构函数的顺序。通常,在类声明中,继承列出的类的顺序决定了它们的构造顺序,然后以相反的顺序被销毁。

虚拟基类意味着在其上使用了虚拟继承:

struct Base {};

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

ASCII 艺术提示:

        Base            Base
         | \             |  \
        /_\ \            |  /_\
         |   \           |    \
         D  /_\          |     D
         |   /           |    /
        /_\ /           /_\ /_\
         | /             |  /
         D1              D2

在这种情况下,多重继承将菱形折叠成一行。但是,这一点仍然被说明。 D1D2 的继承顺序无关紧要。 DBase 的析构函数的调用顺序对它们都是相同的。

【讨论】:

  • 澄清一下,是不是 Base 在顶部,然后是 D1 和 D2,然后是最底部的 D 重新连接钻石?
  • 不,我只是在说明两个半菱形,D1 和 D2。对于它们中的每一个,D 都低于 Base。我是说虽然 D1 声明继承的顺序与 D2 不同,但 D 和 Base 为它们销毁的顺序是相同的。
【解决方案2】:

虚拟基类的关键属性是它们总是在派生类的任何对象中产生一个唯一的基子对象。这正是虚拟基类的特殊,这使得它们不同于可以生成多个子对象的常规基类。

例如,在这个层次结构中

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 {}

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

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

如果我们天真地尝试将先前的销毁计划应用于此层次结构,我们将导致 B 子对象被销毁两次:M1 调用 B 子对象的析构函数,M2调用相同的析构函数 B 子对象。

当然,这是完全不能接受的。每个子对象都必须被破坏一次且仅一次。

为了解决这个问题,当M1M2 用作D 的基础子对象时,明确禁止调用它们的B 子对象的析构函数。调用B的析构函数的责任分配给D的析构函数。类D,当用作一个完全独立的对象(即作为最派生类)时,知道其中只有一个B,并且知道B 的析构函数必须只被调用一次。因此,D 类的析构函数将为B 类型的唯一基子对象调用B 的析构函数。同时,M1M2 的析构函数甚至不会尝试调用B 的析构函数。

这就是虚拟继承的工作原理。你引用的规则基本上就是这样说的。说最后调用虚拟基的析构函数的部分只是意味着每个类的析构函数调用其直接常规基类的析构函数,并且只有在那之后,如果有必要,它会调用其virtual 基类(可能是间接的)。在上面的例子中,D 的析构函数调用了M1M2 的析构函数,然后才调用B 的析构函数。

【讨论】:

    猜你喜欢
    • 2012-06-26
    • 2015-12-15
    • 2015-02-24
    • 2021-04-15
    • 2019-04-20
    • 2014-02-08
    • 2011-09-21
    • 2011-03-30
    • 2011-03-21
    相关资源
    最近更新 更多