编译器如何实现虚拟继承?
在以下代码中:
class A {
public:
A(int) {}
};
class B : public virtual A {
public:
B() : A(1) {}
};
class C : public B {
public:
C() : A(3), B() {}
};
编译器是否生成两个B::ctor
函数实例,一个没有A(1)
调用,一个有它?因此,当从派生类的构造函数调用B::constructor
时,将使用第一个实例,否则使用第二个实例。
答案 0 :(得分:8)
编译器不会创建另一个B构造函数 - 但它会忽略A(1)
。由于A
实际上是继承的,因此首先使用其默认构造函数构造它。并且由于在调用B()
时已经构建了它,A(1)
部分将被忽略。
编辑 - 我错过了A(3)
的构造函数初始化列表中的C
部分。使用虚拟继承时,只有最派生的类初始化虚拟基类。因此,A
将使用A(3)
而不是其默认构造函数构建。其余的仍然有效 - 中间类(此处A
)对B
的任何初始化都将被忽略。
编辑2,尝试回答有关上述实施的实际问题:
在Visual Studio中(至少2010年),使用标志而不是B()
的两个实现。由于B
实际上从A
继承,因此在调用A
的构造函数之前,会检查该标志。如果未设置该标志,则跳过对A()
的调用。然后,在从B
派生的每个类中,标志在初始化A
后重置。如果C
是A
的一部分(如果D
继承自D
,C
将D
,则会使用相同的机制来阻止A
初始化{{1}}初始化{{1}})。
答案 1 :(得分:8)
它依赖于实现。例如,GCC(请参阅this question)将发出两个构造函数,一个调用A(1)
,另一个调用{<1}}。
B1()
B2() // no A
构造B时,称为“完整”版本:
B1():
A(1)
B() body
构造C时,将调用基本版本:
C():
A(3)
B2()
B() body
C() body
实际上,即使没有虚拟继承,也会发出两个构造函数,它们将是相同的。
答案 2 :(得分:3)
我建议你阅读一些文章。这两个非常有趣,特别是第一个来自C ++的父亲:
[1] Bjarne Stroustrup. Multiple Inheritance for C++. The C/C++ Users Journal, May 1999.
我在大学进行多重继承的研讨会(作为学生)时,将它们作为主要参考资料。
答案 3 :(得分:3)
Itanium C++ ABI是所有问题的有用资源,例如“C ++编译器如何实现”。
特别是5.1.4 Other Special Functions and Entities列出了不同目的的不同特殊成员函数:
<ctor-dtor-name> ::= C1 # complete object constructor ::= C2 # base object constructor ::= C3 # complete object allocating constructor ::= D0 # deleting destructor ::= D1 # complete object destructor ::= D2 # base object destructor
1.1 Definitions部分很有用(但不完整):
T类
的基础对象析构函数为T的非静态数据成员和T的非虚拟直接基类运行析构函数的函数。
T类
的完整对象析构函数除了基础对象析构函数所需的操作之外,还运行虚拟基类的析构函数的函数 T。
删除T类
的析构函数除了完整对象析构函数所需的动作之外,还调用适当的释放函数的函数(即,。 对于T。
,运算符删除)
从这些定义中,完整对象构造函数和基础对象构造函数的目的是显而易见的。
答案 4 :(得分:0)
如前所述,它取决于编译器的实现。
但是,通常每次程序员添加新方法时,都会存储在代码中,即使有另一种方法具有相同的id。其他地方(“覆盖”或“超载”)。
每个方法的代码只存储一次,因此如果一个类继承并使用父类中的相同方法,则在内部使用指向代码的指针,它不会复制代码。
如果父类定义了虚方法,并且子类覆盖它,则存储两种方法。每个类都有一个名为“虚方法表”的东西,其中有一个指向每个方法的指针表。
不要担心性能,编译器不会重复方法的代码。