构造函数调用层次结构

7

我在处理一个类型层次结构中的构造函数调用规则时遇到了一些棘手的问题。以下是我的做法:

class A{
protected:
    int _i;
public:
    A(){i = 0;}
    A(int i) : _i(i){}
    virtual ~A(){}
    virtual void print(){std::cout<<i<<std::endl;}
};

class B : virtual public A{
protected:
    int _j;
public:
    B() : A(){_j = 0;}
    B(int i, int j) : A(i), _j(j){}
    virtual ~B(){}
    virtual void print(){std::cout<<i<<", "<<j<<std::endl;}
};

class C : virtual public B{
protected:
    int _k;
public:
    C() : B(){_k = 0;}
    C(int i, int j, int k} : B(i,j), _k(k){}
    virtual ~C(){}
    virtual void print(){std::cout<<i<<", "<<j<<", "<<k<<std::endl;}
};

int main(){
    C* myC = new C(1,2,3);
    myC->print();
    delete myC;
    return 0;
}

现在,我希望新的C(1,2,3)调用B(1,2)的构造函数,然后B(1,2)再调用A(1)的构造函数来存储_i=1,_j=2,_k=3。但是,在创建类C的实例myC时,由于某种我不理解的原因,首先调用的是A的标准构造函数,即A::A();这显然会导致错误的结果,因为受保护变量_i被赋值为0。构造函数A(1)从未被调用。这是为什么呢?我觉得这非常反直觉。有没有一些方法可以避免显式调用类型层次结构中的所有构造函数以实现期望的行为呢?
谢谢你的帮助!

感谢您提供的友善答案。所以,我想我会回去重新阅读Stroustrup有关虚拟继承的概念。似乎默认使用它是不明智的 ;) - user1999920
很多人都在想为什么继承默认不是虚拟的。好吧,你自己找到了答案 :) - Gorpik
4个回答

7
你是否真的需要在这里使用虚拟继承? 问题在于,第一个虚拟基类的构造函数将首先被调用,但是当从B继承C时,你没有指定任何虚拟基类构造函数(后者已经有A作为虚拟继承,所以默认值被调用)。
一种解决方案是删除虚拟继承......如Arne Mertz的答案中所述。 另一个解决方案(如果你确实想要虚拟继承)是从C的构造函数中显式调用A:
C(int i, int j, int k} : A(i), B(i,j), _k(k){}

6
当您使用虚拟继承时,最终派生类必须直接调用其所有虚拟基类的构造函数。在这种情况下,C 的构造函数必须调用 BA 的构造函数。由于您只调用了 B 构造函数,它使用默认的 A 构造函数。无论 B 构造函数是否调用另一个 A 构造函数,都不重要:因为它是虚拟基类,所以该调用将被忽略。
解决此问题有两种方法:显式调用 A(int) 构造函数:
C(int i, int j, int k} : A (i), B(i,j), _k(k){}

或者使用普通继承而不是虚拟继承。

2
最派生类不必显式调用其虚基类的构造函数,它可以让它们隐式地默认初始化,就像问题中所示。无论如何,您是正确的,总是最派生类初始化虚基类。 - CB Bailey
@CharlesBailey 重新阅读我的回答,你是对的:我没有正确解释(明确地是误导性的)。我正在修正它。 - Gorpik

6
这是因为你使用了虚拟继承,这只有在多重继承的情况下才有意义。直接普通继承即可,一切都会按照你的预期进行。

如果A是一个接口,而B扩展了该接口,则虚拟继承确实是唯一正确的解决方案。(当然,如果A是一个接口,它将不会有任何数据成员,因此也没有用户定义的构造函数。通常,虚拟继承仅适用于接口。) - James Kanze
@JamesKanze,这是错误的,如果您不从具有共同祖先的2个或更多类进行多重继承,则不需要虚拟继承。此外,您可以实现多个接口而无需使用虚拟继承:例如,请参见COM。 - Steed
我认为我没有理解你对“接口”的定义。你是指Java中的接口(即没有成员的抽象类)吗?然而,在C++中,我看到了许多不同类型的东西可以称之为接口,但它们都不需要虚继承。当然,在某些情况下,抽象类可能会卷入多重继承图中,但由于它们没有成员,所以vtable应该可以正常工作,即使没有虚继承。 - Arne Mertz
@Steed 当扩展一个接口时,你无法知道是否会有其他扩展,也不知道某个具体类是否想要实现两个或更多的扩展。因此,你必须使用虚继承(除非你处理的是一个小的、封闭的层次结构,在这种情况下,当需要时可以轻松返回并添加虚继承)。 - James Kanze
@ArneMertz 差不多。这是C++中经常出现的模式,Java也一样(除了它在C++中可以工作——您的虚函数仍然可以是私有的,具有非虚拟包装器以检查前置和后置条件)。如果您扩展接口(使用另一个接口),则通常应该进行虚拟继承。 - James Kanze
显示剩余2条评论

0
为什么要声明虚拟继承?如果从类B中删除virtual关键字:virtual public A {...那么你的代码将正常工作。通过声明虚拟A,C将直接调用A()。如果您删除虚拟,则C将不会调用A()。

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