(1)问题引入
C++是允许多继承的,所谓多继承就是一个子类有两个或以上的直接父类。如果存在多继承,那么就一定会存在菱形继承,菱形继承是多继承的一种特殊情况:
菱形继承似乎很实用的,但存在两个严重的问题:冗余性与二义性。以上图为例加以说明:
- 冗余性:
所有的 Assistant 对象都存有两份 Person 基类,从而造成空间冗余浪费- 二义性:
由于所有的 Assistant 对象都存有两份 Person 基类,所以尝试使用Assistant 对象中 Person 的某一成员时就会存在歧义![]()
虽然二义性的问题可以通过作用域分解运算符::
来显式指定,但是冗余的问题始终无法解决,于是C++大佬设计出了虚拟继承的继承方式来解决上述两个问题。
(2)基本概念
虚拟继承的作用是将一个指定的基类的成员实例共享给也从这个基类型直接或间接派生的其它类。
举例来说,如果类 Student 与 Teacher 各自虚继承了类 Person ,那么 Assistant 的对象就只会包含一套类 Person 的实例数据。
在C++中,基类可以在继承方式前加上virtual
来声明虚继承关系,如下:
class Person
{};
class Student :virtual public Person
{};
class Teacher :virtual public Person
{};
class Assistant:public Student, public Teacher
{};
由于虚基类是多个由派生类共享的基类,因此由谁来完成初始化虚基类必须明确。C++标准规定,由最派生类直接初始化虚基类。
(3)底层原理
我们通过下面的代码并观察内存来加以验证:
class Person
{
protected:
int p;
};
class Student :public Person
{
protected:
int s;
};
class Teacher :public Person
{
protected:
int t;
};
class Assistant : public Student, public Teacher
{
public:
int a;
};
int main()
{
Assistant A;
A.a = 1;
A.s = 2;
A.Student::p = 3;
A.t = 4;
A.Teacher::p = 5;
return 0;
}
通过监视我们不难发现,Assistant 直接继承的父类(Student 与 Teacher)都保存有一份 Person ,这就是造成菱形继承冗余性和二义性的“罪魁祸首”。
现在我们使 Student 类和 Teacher 类虚拟继承 Person 基类,再次观察内存,比较和前者的不同(注:这是按32位方式编译的结果,64位则有所不同)
我们可以观察到:
- Person 的成员变量被单独存放,并为所有派生类所共享,从而解决了冗余性和二义性的问题。
- 对于间接继承了虚基类的 Student 和 Teacher ,也必须能直接访问其虚继承来的祖先类,也即应知道其虚继承来的祖先类的地址偏移值。以 Student 为例说明,其中不仅存储了Student 成员变量的值,还存放了一个指针,这个指针也叫虚基表指针。指针所指向空间的下一个位置,便存放着 Student 距离基类 Person 的偏移量,从而也就解决了如何定位的问题。
(4)全文总结
- 有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,而虚拟继承的底层实现相当复杂,所以一般不建议设计出多继承,也一定不要设计出菱形继承,否则程序的复杂度及性能(还需要间接寻址)上都会受到影响
- 多继承可以认为是C++的缺陷之一,很多后来的语言都没有多继承,如Java