我已经开始学习虚拟继承(以及如何解决从具有相同父代的两个父类派生一个类的问题)。为了更好地了解其背后的机制,我举了一个下面的例子:
class A {
public:
A(string text = "Constructor A") { cout << text << endl; }
};
class B: public A {
public:
B(): A("A called from B") { cout << "Constructor B" << endl; }
};
class C : virtual public A {
public:
C() : A("A called from C") { cout << "Constructor C" << endl; }
};
class D : public B, public C {
public:
D() { cout << "Constructor D" << endl; }
};
我有class A
,从class B
派生的A
,实际上从class C
派生的A
和从class D
和B
派生的C
。在主要我只是形成class D
的对象:D d;
,我得到以下输出
Constructor A
A called from B
Constructor B
Constructor C
Constructor D
让我烦恼的是,为什么会有“构造函数A”表示没有被class B
或C
调用。并且为什么在“ Constructor C”之前没有“ A from C”。对于后者,我知道它实际上与class C
的虚拟派生有关,因此我猜它不会再次调用Constructor A
,因为来自class A
的对象已经形成(实际上是两次)。
编辑:
总的来说,我只使一个对象类型为D。
int main() {
D d;
}
首先,由于B
是非虚拟地从A
派生的,所以D
最终有两个A
子对象(好像您根本没有使用virtual
继承一样。
[非虚拟A
由B()
构造(因此为"A called from B"
),并且在构造时虚拟将打印"Constructor A"
。
这是因为虚拟(可能是间接)基的构造函数总是由派生最多的类(D
)的构造函数调用,而不是由任何中间基类(C
)的构造函数调用。
这意味着: A("A called from C")
中的C()
被忽略(因为您没有构造独立的C
)。并且,由于D()
在其成员初始化器列表中未提及A
,因此虚拟A
的构造没有任何参数(就好像由: A()
一样)。
另外,值得一提的是,所有虚拟基础都是constructed before非虚拟基础。
因此您的代码保证可以在Constructor A
之前打印A called from B
。
您的代码忘记了什么,并且A
对象中仍然有两个D
对象。您可以通过将公共int test
成员添加到A
并尝试以下代码来验证此声明。您将获得两个不同的地址:
D d;
cout << &d.B::test <<endl; // the non virtual A subobject of B
cout << &d.C::test <<endl; // the virtual A subobject of C
所有类虚拟共享A
并直接从A
继承必须声明虚拟继承。因此,您需要更正B
类:
class B: virtual public A {...} // you forgot the virtual here
上面的代码段可以按预期工作,您甚至可以寻址d.test
而不会出现任何歧义错误。 Online demo
C ++规则要求每个对象都带有虚拟A
子对象,以为A
提供构造函数。对于您的情况,D
应该提供虚拟A
的结构。
由于没有显式构造函数,因此D
将查找默认构造函数A
。它找到一个,因为您为A
构造函数提供了默认参数。这具有误导性,因为它实际上告诉您“构造函数”是D
在使用它。
如果删除此默认参数,则代码将不再编译。然后,您需要类似以下内容来正确构建A
:
class A {
public:
int test;
A(string text) { cout << "A is constructed: "<<text << endl; }
};
class D : public B, public C {
public:
D() : A("Mandatory, if there is no default consructor") { cout << "Constructor D" << endl; }
};
为什么这样的C ++构造规则?
因为当您具有虚拟继承时,没有理由将A
的B
构造画在C
的构造上。也不相反。另一方面,B
和C
都定义了如何构造其A
子对象。为了解决选择正确结构时的歧义,决定了该规则。如果虚拟类没有默认构造函数,可能会很痛苦。 [当类型具有虚拟基数时,虚拟基数是从最派生类型的构造函数构造的。因此,从D的构造函数中调用了“构造函数A”。