虚继承
首先, 虚继承和虚函数是完全无相关 的两个概念
多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题, 命名冲突就是不可回避的一个。
为什么要了解虚继承这一概念?因为上一节的多继承示例程序存在着一些隐患:
多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承,如下图所示:
在多重继承中,如果发生了例如:类B继承类A,类C也继承类A,类D又同时继承了类B和类C的情况下,这个时候类A中的变量成员和函数成员继承到类D中就变成了两份,一份来自A-->B-->D
,另一份来自A-->C-->D
这条路径。
在一个派生类(D)中保留间接基类(A)的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的 : 因为保留多份间接基类的成员变量 不仅占用较多的存储空间,还容易产生命名冲突
例如 类A(间接基类) 有一个成员变量name
,那么在 类D(最终派生类) 中 直接访问 name
这个继承来的变量就会出现歧义(若想访问此变量 需加类名和域解析符::
例如[ A::name
]),因为编译器不知道它继承的这个name
变量是从A -->B-->D
这条路径,还是来自 A-->C-->D
这条路径继承来的,这就是 多继承产生的歧义性问题 ,这在程序中是不能容忍的,解决这个问题的方法就是利用虚继承。
虚继承与访问歧义
先看多继承中案例中的程序:
Teacher和Student类的个人介绍程序:
本案例中 Person基类派生Teacher和Student两个类,TeachingStudent这个类又派生自Teacher和Student两个类,完成了一个典型的菱形继承
TeachingStudent类的个人介绍程序:
首先,在上个案例中 TeachingStudent类的 TeachingStudent::introduce()
(兼职教师的学生杰克的个人介绍程序) 里,对于class这个属性(上课和授课信息属性 ),因为一个兼职学生教一门课和上一门课是不可能一样的,所以应该明确的告诉编译器应该使用哪一个class属性(即应该明确指定是使用派生基类student还是派生基类teacher的class属性)
而关于兼职学生麦克的name(姓名属性),由于麦克只有一个名字,所以完全没必要告诉编译器该使用student类中的名字属性还是teacher类中的,但如今由于TeachingStudent类继承了两份Person类的成员name,所以关于杰克的名字(name属性),我们 不得不明确的告诉编译器应使用哪一个name属性(也就是需要让编译器知道这个 name属性 是从Person-->Teacher
这条路径还是从Person-->Student
这一条路径继承来的),这便是多继承导致的歧义性。
为了解决多继承导致的这个问题, 使得在最终派生类[TeachingStudent]中只保留一份间接基类[Person]的成员name ,我们在派生类TeachingStudent
的直接基类(Teacher
和Student
的继承方式前面加上virtual关键字,这便是虚继承
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 Person类就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,最终派生类中只会包含一份虚基类的成员。
虚基类(Person类)成员的可见性
因为在虚继承的最终派生类(TeachingStudent)中只保留了一份虚基类的成员,所以该虚基类中成员(Person中的name成员)是可以直接被最终派生类访问的,不会产生二义性
如果虚基类的成员被一条派生路径所覆盖(A->B->C
),那么最终派生类仍可以直接访问虚基类的成员,但是当虚基类的成员被两条以上的派生路径所覆盖(A->B->C|A->D->C
),就不能直接访问了,最终派生类在访问虚基类的成员的时候必须使用 [类名+域解析符::
] 指明该对象属于哪个类。
进行虚继承过后的继承关系图:
以上图的菱形继承为例,假设 A 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:
如果 B 和 C 中都没有 x 的定义:
那么 x 将被解析为 A 的成员,此时不存在二义性,最终派生类D可以直接访问x这个变量,且这个变量属于A::x
如果 B 或 C 其中的B类定义了 x