在 c++ 中的菱形问题中,为啥我们需要从子类中调用祖父构造函数?

Posted

技术标签:

【中文标题】在 c++ 中的菱形问题中,为啥我们需要从子类中调用祖父构造函数?【英文标题】:In a diamond problem in c++ , Why do we need to call grand_parent constructor from child class?在 c++ 中的菱形问题中,为什么我们需要从子类中调用祖父构造函数? 【发布时间】:2019-07-26 06:56:59 【问题描述】:

请阅读代码了解情况。

#include <iostream>
using namespace std;
class one

protected:
    int x;
public:
    one(int a)
    
        x=a;
        cout << "one cons called\n";
    
    void display(void)
    
        cout << "x = " << x << endl;
    
    ~one()
    
        cout << "one destroy\n";
    
;
class two : virtual protected one

protected:
    int y;
public:
    two(int a,int b) : one(a),y(b)
    
        cout << "two cons called\n";
    
    void display(void)
    
        one::display();
        cout << "y = " << y << endl;
    
    ~two()
    
        cout << "two destroy\n";
    
;

class three : protected virtual one

protected:
    int z;
public:
    three(int a,int b) : one(a),z(b)
    
        cout << "Three cons called\n";
    
    void display(void)
    
        one::display();
        cout << "z = " << z << endl;
    
    ~three()
    
        cout << "three destroy\n";
    
;

class four : private two, private three

public:
    four(int a,int b,int c) :one(a), two(a,b),three(a,c)
    
        cout << " four cons called\n";
    
    void display(void)
    
        one::display();
        cout << "y = " << y << endl;
        cout << "z = " << z << endl;
    
    ~four()
    
        cout << "four destroy\n";
    
;
int main()

    four ob(1,2,3);
    ob.display();
    return 0;

如果我替换代码

four(int a,int b,int c) :one(a), two(a,b),three(a,c)

four(int a,int b,int c) :two(a,b),three(a,c)

一个错误消息,例如:在我的代码块 ide 中没有调用 'one::one()' 的匹配函数。

如您所见,这是基于菱形问题的代码。其中第一类是祖父类。二级和三级作为父级,四级作为子级。所以我使用 virtual 关键字来避免歧义。我在这里理解的一切,除非一件事。我知道当父类具有参数化构造函数时,我们需要从派生类向该构造函数提供参数。那么为什么需要向构造函数一提供参数,其中类 4 只有 2 个父类,即 2 和 3 。 如果我不调用第四类中的构造函数一,代码会给我编译时错误。请解释一下为什么我们需要这样做。

【问题讨论】:

您展示的代码和类有一些可疑之处。例如,为什么要在所有类中覆盖 non-虚拟函数 display?为什么你有private 继承?通常建议不要使用私有继承以支持组合。 @Someprogrammerdude 。我写这段代码是为了学习,不是为实际项目写的 可能重复:***.com/questions/44324583/… 【参考方案1】:

您的层次结构中的virtual 继承消除了基类one 的存在歧义,方法是确保one 的单个实例仅存储在twothree 的子类中。回想一下,当继承某个类时,派生实例将始终在内部存储基实例 - 因此virtual 继承确保twothree 内部的one 实例在某种程度上被任何类进一步“覆盖”继承层次结构。

现在的问题是:谁负责初始化这个单一的one 实例?应该是two 还是three?显然不是他们两个,因为只有一个实例。你在这里:它总是最派生的类负责初始化one - 这是有道理的:嵌入基类副本的实例必须初始化它。

这是没有fourfour加上virtual继承的类层次结构的样子:

              +----------+                           +----------+
              |   one    |                           |   one    |
              +----+-----+                           +----+-----+
                   |                                      |
                   |                                      |
         +-------+-----------+           virtual +--------+--------+ virtual
         |                   |                   |                 |
         |                   |                   |                 |
+--------+-------+   +-------+-------+      +----+----+       +----+----+
|      two       |   |      three    |      |  two    |       |  three  |
| +------------+ |   | +----------+  |      +----+----+       +----+----+
| |   one      | |   | |   one    |  |           |                 |
| +------------+ |   | +----------+  |           +--------+--------+
|  => must init! |   | => must init! |                    |
+----------------+   +---------------+            +-------+--------+
                                                  |     four       |
                                                  | +------------+ |
                                                  | |    one     | |
                                                  | +------------+ |
                                                  | => must init!  |
                                                  +----------------+

您可以这样想这种机制:virtual 继承提供了基类实例 virtual-ness,其中包括构造实例 - 这种责任在层次结构中向下传递。

【讨论】:

您是手动创建的还是使用工具创建的? @0x499602D2 初稿here,然后在vim中进行一些可视化块编辑。【参考方案2】:

假设你有以下钻石:

     Base
    /    \
 Left    Right
    \    /
     Down

Base 类可以非常简单,它有一个由构造函数初始化的 int 成员:

struct Base

    Base(int x) 
        : x(x)
    
    virtual ~Base() = default;
    int x;
;

由于Left 继承自Base,它的构造函数可以将参数传递给Base 构造函数。在这里,如果你构造一个Left 对象,它的x 成员将是1

struct Left : virtual Base

    Left() : Base(1)
    
;

另一个类Right也继承自Base。这意味着它的构造函数也可以将参数传递给Base 构造函数。在这里,它的x 成员将是2

struct Right : virtual Base

    Right() : Base(2)
    
;

现在有趣的部分来了:如果你同时继承 LeftRight 会发生什么?

// This does not compile.
struct Down : Left, Right

    Down() : Left(), Right()
    
;

LeftRight 都调用 Base 构造函数,但它们使用不同的参数。编译器现在应该使用来自LeftBase(1) 部分还是应该使用来自RightBase(2) 部分?答案很简单:两者都不使用!编译器将选择权留给您,并允许您指定应该使用哪个构造函数:

// Hooray, this version compiles.
struct Down : Left, Right

    Down() : Base(42), Left(), Right()
    
;

【讨论】:

【参考方案3】:

当一个类的两个超类有一个共同的基类时,就会出现 Diamond 问题。 这个问题的解决方案是“虚拟”关键字。一般情况下不允许直接调用祖父的构造函数,必须通过父类调用。只有当我们使用“虚拟”关键字时才允许。 所以,当我们使用‘virtual’关键字时,即使父类显式调用参数化构造函数,也会默认调用祖父类的默认构造函数。

【讨论】:

以上是关于在 c++ 中的菱形问题中,为啥我们需要从子类中调用祖父构造函数?的主要内容,如果未能解决你的问题,请参考以下文章

一文详解C++类的内存布局和虚函数底层实现机制

菱形继承

为啥 c++ 允许直接从子类中调用祖父类方法?

C++继承之C++中不同的继承体系

C++继承之C++中不同的继承体系

C++继承之C++中不同的继承体系