多重继承与虚继承
多重继承(multiple inheritance)是指从多个直接基类中产生派生类的能力。多重继承的派生类继承了所有父类的属性。
多重派生
在派生类的派生列表中可以包含多个基类:
|
|
和只有一个基类的继承一样,多重继承的派生列表也只能包含已经被定义过的类,而且这些类不能是final的。对于派生类能够继承的基类个数,C++没有进行特殊规定;但是在某个给定的派生列表中,同一个基类只能出现一次。
多重继承的派生类从每个基类中继承状态
在多重继承关系中,派生类的对象包含有每个基类的子对象。在panda对象中含有Bear部分(其中又含有ZooAnimal部分)、一个Endangered部分以及在Panda中声明的非静态数据成员。
派生类构造函数初始化所有基类
构造一个派生类的对象将同时构造并初始化它的所有基类子对象。但只能初始化它的直接基类:
|
|
派生类的构造函数初始化列表将实参分别传递给每个直接基类。其中基类的构造顺序与派生列表中基类出现顺序一致,而与派生类构造函数初始值列表中基类的顺序无关。
一个Panda对象按照如下词序进行初始化:
- ZooAnimal是整个继承体系的最终基类,Bear是Panda的直接基类,ZooAnimal是Bear的基类,所以首先初始化ZooAnimal。
- 接下来初始化Panda的第一个直接基类Bear。
- 然后初始化Panda的第二个直接基类Endangered。
- 最后初始化Panda。
继承的构造函数与多重继承
在C++11新标准中,允许派生类从它的一个或几个基类中继承构造函数。但是如果从多个基类中继承了相同的构造函数(即形参列表完全相同),则程序将产生错误:
|
|
如果一个类从它的多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义它自己的版本:
|
|
析构函数与多重继承
和往常一样,派生类的析构函数只负责清除派生类本身分配的资源,派生类的成员及基类都是自动销毁的。合成的析构函数体为空。
析构函数的调用顺序正好与构造函数相反。
类型转换与多个基类
我们可以令某个可访问基类的指针或引用直接指向一个派生类对象。
与只有一个基类的继承一样,对象、指针或引用的静态类型决定了我们能够使用哪些成员。
如果我们使用一个ZooAnimal指针,则只有定义在ZooAnimal中的操作是可以使用的,Panda接口中的Bear、Panda和Endangered特有的部分都不可见。
多重继承下的类作用域
在只有一个基类的情况下,派生类的作用域嵌套在直接和间接基类的作用域中。查找过程沿着继承体系自底向上进行,直到找到所需的名字。派生类的名字将隐藏基类的同名成员。
在多重继承的情况下,相同的查找过程在所有直接基类中同时进行。如果名字在多个基类中都被找到,则对该名字的使用将具有二义性。
虚继承
尽管在派生列表中同一个基类只能出现一次,但实际上派生类可以多次继承同一个类。派生类可以通过它的两个直接基类分别继承同一个间接基类,也可以直接继承某个基类,然后通过另一个基类再一次间接继承该类。
在默认情况下,派生类中含有继承链上每个类对应的子部分。如果某个类在派生过程中出现了多次,则派生类中将包含该类的多个子对象。
为避免产生多个子对象,在C++语言中我们通过虚继承(virtual inheritance)的机制解决上述问题。虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类(virtual base class)。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。
note: 虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。
使用虚基类
我们指定虚基类的方式是在派生列表中添加关键字virtual:
|
|
通过上面的代码我们将ZooAnimal定义为Raccoon和Bear的虚基类
virtual说明符表明了一种愿望,即在后续的派生类当中共享虚基类的同一份实例。至于什么样的类能够作为虚基类并没有特殊规定。
如果某个类指定了虚基类,则该类的派生仍按常规方式进行:
|
|
Panda中只有一个ZooAnimal基类部分
虚基类成员的可见性
因为在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,并不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,则我们仍然可以直接访问这个被覆盖的成员。但是如果被多余一个基类覆盖,则一般情况下派生类必须为该成员自定义一个新的版本。
与非虚的多重继承体系一样,解决这种二义性问题最好的方法是在派生类中为成员自定义新的示例。
构造函数与虚继承
在虚派生中,虚基类是由最底层的派生类(也就是最最后面的派生类)初始化的。,当创建Panda对象时,由Panda的构造函数独自控制ZooAnimal的初始化过程。
为了理解这一规则,我们不妨假设当以普通规则处理初始任务时会发生什么情况。在此例中,虚基类将会在多条继承路径上被重复初始化。以ZooAnimal为例,如果应用普通规则,则Raccoon和Bear都会试图初始化Panda对象的ZooAnimal部分。
当创建一个Bear(或Raccoon)的对象时,将直接初始化其ZooAnimal基类部分。
当创建一个Panda对象时,Panda位于派生的最低层并由它负责初始化共享的ZooAnimal基类部分。即使ZooAnimal不是Panda的直接基类,Panda的构造函数也可以初始化ZooAnimal。
|
|
虚继承的对象的构造方式
含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序依次对其进行初始化。
例如:当我们创建一个Panda对象时:
- 首先使用Panda的构造函数初始值列表中提供的初始值构造虚基类ZooAnimal部分。
- 接下来构造Bear部分
- 然后构造Raccoon部分
- 然后构造第三个直接基类Endangered
- 最后构造Panda部分
如果Panda没有显式地初始化ZooAnimal基类,则ZooAnimal的默认构造函数将被调用。如果ZooAnimal没有默认构造函数,则代码将发生错误。
note: 虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关。
构造函数与析构函数的次序
一个类可以有多个虚基类。此时,这些虚的子对象按照它们在派生列表中出现的顺序从左向右依次构造。例如,在下面这个稍显杂乱的TeddyBear派生关系中有两个虚基类:ToyAnimal是直接虚基类,ZooAnimal是Bear的虚基类:
|
|
编译器按照直接基类的声明顺序对其依次进行检查,以确定其中是否含有虚基类。如果有,则先构造虚基类,然后按照声明的顺序逐一构造其他非虚基类。因此,创建一个TeddyBear对象,需要按照如下次序调用这些构造函数:
|
|
合成的拷贝和移动构造函数按照完成相同的顺序执行,合成的赋值运算符中的成员也按照该顺序赋值。和往常一样,对象的销毁顺序与构造顺序正好相反,首先销毁TeddyBear部分,最后ZooAnimal部分。