【问题标题】:virtual inheritance in c++ misunderstoodc++中的虚拟继承被误解
【发布时间】:2014-09-15 16:02:54
【问题描述】:

我读到在虚拟继承中,构造函数被称为“来自最派生的”。 考虑以下代码。在我看来,这里派生最多的类是 D。然后是 B 和 C,“最不派生的”是 A。 那么为什么最“基本”的构造函数首先被调用而不是“最衍生的”呢? 谢谢。

#include <iostream>
using namespace std;

struct A
{
    A()
    {
        cout << "A default constructor" << endl;
    }
};

struct B : virtual public A
{
    B() : A()
    {
        cout << "B default constructor" << endl;
    }
};

struct C : virtual public A
{
    C() : A()
    {
        cout << "C default constructor" << endl;
    }
};

struct D : public B, public C
{
    D() : B(), C()
    {
        cout << "D default constructor" << endl;
    }
};

int main()
{
    D d;
}

这是输出:

A default constructor
B default constructor
C default constructor
D default constructor

更新:


好的。所以考虑下面的代码。 请注意,尽管 D、B 和 C 构造函数发送了 7,但仍打印了 10。 这里实际上基类 IS 是第一个被调用的。 没有从 D 到 B 到 A 的链条。 A() 首先被调用(实际上它是默认构造函数)。然后才调用 B 和 C 构造函数。

但我读到:“从最衍生。” 来源:c++ virtual inheritance

这里派生最多的是 D,然后是 B 和 C,然后是 A。那么为什么不考虑参数 B、D、C 从它们的构造函数传递给它的情况下首先调用 A 呢? 谢谢。

代码:

#include <iostream>
using namespace std;

struct A
{
    int _x;
    A()
    {
        _x = 10;
        cout << "A default constructor" << endl;
    }
    A(int x)
    {
        _x = x;
        cout << "A NOT-default constructor" << endl;
    }
};

struct B : virtual public A
{
    B(int x=7) : A(x)
    {
        cout << "B  constructor" << endl;
    }
};

struct C : virtual public A
{
    C(int x=7) : A(x)
    {
        cout << "C  constructor" << endl;
    }
};

struct D : public B, public C
{
    D(int x=7) : B(x), C(x)
    {
        cout << "D  constructor" << endl;
    }
};

int main()
{
    D d;
    cout << d._x;
}

输出:

A default constructor
B  constructor
C  constructor
D  constructor
10

【问题讨论】:

  • 你在哪里读到的?如果构造函数是按从多到少的顺序调用的,那么任何构造函数都不能引用基类中的任何内容,因为实例的基础部分还没有被构造!您收到的输出是正确的。
  • 最派生的构造函数首先按源文件顺序初始化虚拟基类。困难在哪里?
  • 问题是你在构造函数的末尾输出。首先调用派生构造函数,但它调用父构造函数然后您的输出。
  • 直接回答您的问题:最派生的构造函数 is 被首先调用,但在它执行第一行代码之前,它还调用基类构造函数。这就是为什么它不首先打印"D constructor"

标签: c++ inheritance virtual-inheritance


【解决方案1】:

C++ 中的构造顺序非常简单:

  1. 您调用了最衍生的 ctor(通过初始化变量;auto-storage-classstatic、动态等)。
  2. 那个ctor初始化所有子对象:
    • 委托 ctor 调用另一个最衍生的 ctor。
    • 非委派 ctor 自己完成工作:
      1. 如果是派生最多的 ctor,virtual 以左优先声明顺序为基础。 (这意味着由 non-most-derived-ctors 给出的 virtual-base ctor 的参数将被忽略)
      2. 左优先声明顺序中的其他直接基数。
      3. 声明顺序的成员。
  3. 演员的身体跑了。

由于你的 most-base ctor 是唯一的虚拟基础,它的主体是第一个运行的 ctor-body,由最衍生的 ctor 直接调用。

一般来说,调用 virtual 函数、typeiddynamic_cast 是安全的,尽管在基子对象全部初始化之前:May I call a virtual function to initialize a base-class sub-object?

【讨论】:

  • 如果不简单,也很直观。
【解决方案2】:

这意味着最派生类有责任初始化任何虚拟基础子对象,以及它直接派生的那些。也就是说,所有基础构造函数都是从派生最多的构造函数中调用的:D 的构造函数调用A 的构造函数,然后是BC,最后进行自身初始化。这对于确保共享基础对象只初始化一次是必要的,并且在从它派生的任何类之前。

这并不意味着 顺序 是从最多到最少派生的。与常规继承一样,基子对象总是首先初始化,因此在初始化派生类时它们是可用的。

为了回答您更新的问题,由于D 初始化A,它将调用默认构造函数,除非其初始化列表包含A 的条目:

D(int x=7) : B(x), C(x)        // calls A(), initialising with 10
D(int x=7) : A(x), B(x), C(x)  // calls A(int), initialising with 7

B(或C)的初始化器列表中A 的任何条目仅在B(或C)是最派生类时使用(因此负责初始化@987654334 @)。

【讨论】:

  • 在这种情况下,所有的基础 ctor 都由最派生的 ctor 调用。但仅仅是因为单个非直接基是virtual
  • @Deduplicator:我已将其改写为更准确。
【解决方案3】:

如果派生类的构造函数代码在构造基类之前运行,你会如何建议派生类的行为?虽然技术上可行,但完全没用。

您观察到的行为是唯一正常的行为,与虚拟继承无关。

【讨论】:

    【解决方案4】:

    你说

    我读到在虚拟继承中,构造函数被称为“从最派生的”。

    确实如此。让我详细说明。

    在您的情况下,D 是派生最多的。

    当您构造D 的实例时,A 的构造函数会从D 的构造函数中调用,因为D 的每个实例只有A 的实例。不会从BC 的构造函数中调用A 的构造函数。

    当您构造B 的实例时,A 的构造函数会从B 的构造函数中调用。对于C 的实例也是如此。

    如果您有 D 的子类型,

    struct E : public D
    {
    };
    

    并且你创建了一个E的实例,那么A的构造函数就会被E的构造函数调用。

    C++ 草案标准 (N3337) 说明了涉及虚拟基类的初始化:

    12.6.2 初始化基和成员

    5 初始化应按以下顺序进行:

    ——首先,并且仅对于如下所述的最派生类的构造函数,虚拟基类应按照它们在基类的有向无环图的深度优先从左到右遍历中出现的顺序进行初始化,其中“从左到右”是派生类base-specifier-list中基类名称的出现顺序。

    【讨论】:

      【解决方案5】:

      这就是构建对象的方式。

      更新

      看看这个例子:

      class A{
          A()
          {
          cout << "Constructor A"<<endl;
          }
      
          ~A()
          {
          cout << "Destructor A"<<endl;
          }
      }
      
      class B : public A{
          B()
          {
          cout << "Constructor B"<<endl;
          }
      
          ~B()
          {
          cout << "Destructor B"<<endl;
          }
      }
      class C : public B{
          C()
          {
          cout << "Constructor C"<<endl;
          }
      
          ~C()
          {
          cout << "Destructor C"<<endl;
          }
      }
      

      创建C类的对象:

      C obj;
      

      输出如下:

      构造函数 A
      构造函数 B
      构造函数 C
      析构函数 C
      析构函数 B
      析构函数A

      执行的原因是这样的:

      当一个类派生自另一个类时,它会派生该类的属性。派生类的功能可能会或可能不会取决于基类的功能,但绝不可能相反。假设派生类依赖于基类功能,那么在初始化派生类之前正确初始化基类是很重要的。

      更新:

      C的一个对象被创建时,在C的构造函数可以执行之前,他从C构造函数的控制权转移到它的基类的构造函数。这就是我所说的基类的意思。

      更新:

      通过绘制对象关系可以最好地回答您的问题。
      A 位于顶部,D 位于底部。

      “在虚拟继承中,[虚拟基]构造函数是从最派生的”[类型的构造函数]调用的。 "

      通过上述声明,他们要求您从最派生类型的构造函数 (D) 开始,并一直到最基类的构造函数 (A)。

      更新:

      @leemes 让 cmets 中的执行流程更加清晰:

      构造函数本身“重定向”到基的构造函数。返回后,它继续使用自己的构造函数。你错过的是花括号中写的不是整个实现。这只是在调用基本 ctor 并初始化成员变量之后发生的事情。与 ctor 相同:在调用成员变量的 ctor 和基类的 ctor 之前,您在花括号中写入要执行的内容。

      【讨论】:

      • "在虚拟继承中,[虚拟基]构造函数是从最派生的"[类型的构造函数]调用的。 “这里有明显的混淆,你没有解决
      • "构造函数从基类执行到派生类,析构函数从派生类到基类执行。"不,恰恰相反。
      • 显然你误会了我,所以这次我将通过示例进行尝试:coliru.stacked-crooked.com/a/74ab1c082de9d984 Virtual-base-constructors 是从 mem-initializer-list 中调用的,用于最衍生类型的-构造函数。
      • @gldraphael 仍然不正确。构造函数本身“重定向”到基的构造函数。返回后,它继续使用自己的构造函数。你错过的是花括号中写的不是整个实现。这只是在调用基础ctor 和初始化成员变量之后 发生的。与 ctor 相同:您在花括号中写下要执行的内容调用成员变量的 ctor 然后调用基的 ctor。
      猜你喜欢
      • 2016-03-26
      • 2011-01-08
      • 1970-01-01
      • 2011-07-07
      • 2017-03-31
      相关资源
      最近更新 更多