虚继承是解决 C++ 多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:第一,浪费存储空间;第二,存在二义性问题。
针对这种情况,C++ 提供虚基类(virtual base class)的方法,使得在继承间接共同基类时只保留一份成员。方法如下:
class A // 声明基类A
{
// 代码
};
class B: virtual public A // 声明类 B 是类 A 的公有派生类,A 是 B 的虚基类
{
// 代码
};
class C: virtual public A // 声明类 C 是类 A 的公有派生类,A 是 C 的虚基类
{
// 代码
};
class D: public B, public C // 类 D 中只有一份 A 的数据
{
// 代码
};
【注意】虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。
问题:如何对虚基类进行初始化呢?
如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类)中,通过构造函数的初始化列表对虚基类进行初始化。如下:
class A // 声明基类 A
{
A(int i); // 声明一个带有参数的构造函数
};
class B: virtual public A // A 是 B 的虚基类
{
B(int n):A(n){ } // B 类构造函数, 在初始化列表中对虚基类 A 进行初始化
};
class C: virtual public A // A 是 C 的虚基类
{
C(int n):A(n){ } // C 类构造函数, 在初始化列表中对虚基类 A 进行初始化
};
class D: public B, public C
{
D(int n):A(n),B(n),C(n){ } // D 类构造函数, 在初始化列表中对所有基类进行初始化
};
【注意】在定义类 D 的构造函数时,与以往使用的方法有所不同。以往,在派生类的构造函数中只需负责对其直接基类初始化,再由其直接基类负责对间接基类初始化。现在,由于虚基类在派生类中只有一份数据成员,所以这份数据成员的初始化必须由派生类直接给出。如果不由最后的派生类直接对虚基类初始化,而由虚基类的直接派生类(如类 B 和类 C)对虚基类初始化,就有可能由于在类 B 和类 C 的构造函数中对虚基类给出不同的初始化参数而产生矛盾。所以规定:在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。
问题:类 D 的构造函数通过初始化表调了虚基类的构造函数 A,而类 B 和类 C 的构造函数也通过初始化表调用了虚基类的构造函数 A,这样虚基类的构造函数岂非被调用了 3 次?
这点不必过虑,C++ 编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类 B 和类 C)对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。
每个虚继承的子类都有一个虚基类表指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)。虚基类表指针(virtual base table pointer)指向虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址,通过偏移地址,就找到了虚基类成员。
在这里我们可以对比虚函数的实现原理:
- 它们有相似之处,都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)。
- 虚基类依旧存在继承类中,占用存储空间;虚函数不占用存储空间。
- 虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。
占用内存计算:
class A // 大小为 4
{
public:
int a;
};
class B :virtual public A // 大小为 12,变量 a, b 共 8 字节,虚基类表指针 4
{
public:
int b;
};
class C :virtual public A // 与 B 一样 12
{
public:
int c;
};
class D :public B, public C // 24, 变量 a, b, c, d 共 16,B 的虚基类指针 4,C 的虚基类指针 4
{
public:
int d;
};