为啥这个钻石类继承输出不是我期望的?

Posted

技术标签:

【中文标题】为啥这个钻石类继承输出不是我期望的?【英文标题】:Why is this diamond class inheritance output not what I expect?为什么这个钻石类继承输出不是我期望的? 【发布时间】:2017-04-24 06:29:04 【问题描述】:

考虑:

#include <iostream>

using namespace std;

class A // base class
private:
    int data;
public:
    A(int data = 0) 
    
        this->data = data;
    
    void show() 
    
        cout << data << endl;
        return;
    
;

class B : virtual public A 
public:
    B(int data = 0) :
        A(data) 
    
;

class C : virtual public A 
public:
    C(int data = 0) :
        A(data) 
    
;

class D : public B, public C 
public:
    D(int dataB = 0, int dataC = 0) :
        B(dataB),
        C(dataC) 
    
;

int main() 
    D d(1, 2);
    d.B::show();
    d.C::show();
    return 0;

以上代码为菱形类继承图。基类是 A。我使用虚拟继承来避免菱形问题。但是为什么这个程序的输出是0,0,而不是我期望的1,2

B 的构造函数被传递给data=1,并在其初始化列表中调用AdataC 的构造函数类似通过 data=2 及其初始化列表调用 Adata

然后我们询问BC 子对象show 的值。我们得到的是00而不是12

【问题讨论】:

你可以很容易地拥有 1,2 - 只是让继承不是虚拟的 :) 然后你将拥有 A 的两个副本。 无论如何this-&gt; 是奇怪的。在现代 C++ 中,您可以编写 A(int data = 0): data(data) ,'构造函数初始化列表',这样更简洁,并且允许您拥有常量成员。 这不是问题,但您真的需要std::endl 需要的额外内容吗? '\n' 结束一行。 C++: “std::endl” vs “\n” 【参考方案1】:

当你有这个带有虚拟继承的方案时,由层次结构中最派生的类(在本例中为D)来调用公共基础的构造函数(A1,2 .

由于A 的构造函数有一个默认参数data = 0,它可以用作默认构造函数。这就是正在发生的事情,常见的 A 子对象被默认构造,因为您从 D 的成员初始化列表中省略了它。

如果您删除 data 的默认值,您将得到一个很好的编译器错误以强调:

A(int data) 

    this->data = data;

On g++ it results with:

main.cpp: In constructor 'D::D(int, int)':
main.cpp:37:16: error: no matching function for call to 'A::A()'
         C(dataC) 

1 请记住,对于虚拟继承,只有 一个 类型为 A 的子对象。 BC 子对象都引用它。您对show 的调用不可能打印不同的内容,因为它们访问的是相同的 数据。这就是为什么它取决于最派生的类,所以没有歧义。

[class.mi/4]

不包含关键字 virtual 的基类说明符指定非虚拟基类。包含关键字 virtual 的基类说明符指定虚拟基类。对于最派生类的类格中非虚拟基类的每次不同出现,最派生对象应包含该类型的相应不同基类子对象。 对于每个指定为虚拟的不同基类,最派生的对象应包含该类型的单个基类子对象。

[class.base.init/13.1]

在非委托构造函数中,初始化按以下顺序进行:

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

...

2 因此,如果您想使用特定数据构造A,您可以像这样定义D::D()

D(int dataA = 0) :
  A(dataA) 

【讨论】:

【参考方案2】:

当您拥有virtual 继承时,virtual 基类由最派生类的构造函数初始化。

D(int dataB = 0, int dataC = 0) :
    B(dataB),
    C(dataC) 

相当于:

D(int dataB = 0, int dataC = 0) :
    A(),
    B(dataB),
    C(dataC) 

在您的情况下,与

相同
D(int dataB = 0, int dataC = 0) :
    A(0),
    B(dataB),
    C(dataC) 

除非你构造一个B的实例,

B(int data = 0) :
    A(data) 

相同
B(int data = 0) 

没有代码初始化A,因为A 已经在D 的构造函数中初始化。

C::C(int data) 的实现也是如此。

这解释了您所看到的输出。

【讨论】:

【参考方案3】:

我认为您误解了virtual 继承和“钻石问题”的概念。使用virtual 继承,您可以创建菱形继承模式,但从您的帖子看来,您希望避免这种情况,而是有两个基数A,一个来自B,另一个来自C。要实现这一点,只需避免 BC 中的虚拟继承,因为您的代码将编写 1 2

顺便说一句,如果只有B 具有从A 继承的virtualCA 常规继承,那么D 将有两个基数A,但又是来自B默认初始化(如其他答案中所述)。

【讨论】:

以上是关于为啥这个钻石类继承输出不是我期望的?的主要内容,如果未能解决你的问题,请参考以下文章

避免钻石继承[重复]

非 Diamond 类型中的虚拟继承

python多重继承的钻石问题

请问含有多个对象成员的派生类的构造函数执行时不是先执行基类么?为啥这个先输出的是“正式生是”这个

Unity脚本类为啥要尽量避免继承MonoBehaviour类

C++菱形继承问题与虚拟继承原理