为什么要有虚继承?是为了解决什么问题?

虚继承是解决 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;  
};  

你可能感兴趣的:(为什么要有虚继承?是为了解决什么问题?)