18.3.1 多重继承
多重继承是指从多个直接基类中产生派生类的能力。多重继承的派生类继承了所有父类的属性
派生类的派生列表中可以包含多个基类。每个基类都包含一个可选的访问说明符。和单继承相同,如果访问说明符被省略,则关键字class
对应的默认访问说明符是private
,关键字struct
对应的是public
class Bear : public ZooAnimal { /* ... */ };
class Panda : public Bear, public Endangered { /* ... */ };
这里Panda
继承自Bear
和Endangered
,Bear
又继承自ZooAnimal
和单继承相同,多重继承的派生列表也只能包含已经被定义过的类,且这些类不能是final
的
多重继承关系中,派生类对象包含每个基类的子对象
派生类的构造函数初始化所有基类
构造一个多重继承的派生类对象将同时构造并初始化它的所有基类子对象,多重继承的派生类的构造函数初始化只能初始化它的直接基类
// 显式地初始化所有基类
Panda::Panda(std::string name, bool onExhibit)
: Bear(name, onExhibit, "Panda"),
Endangered(Endangered::critical) { }
// 隐式地使用Bear的默认构造函数初始化Bear子对象
Panda::Panda()
: Endangered(Endangered::critical) { }
派生类的构造函数初始值列表将实参分别传递给每个直接基类。其中基类的构造顺序与派生列表中基类的出现顺序一致,与构造函数初始值列表中基类的顺序无关
ZooAnimal
是整个继承体系的最终基类,Bear
是Panda
的直接基类,ZooAnimal
是Bear
的基类,所以首先初始化ZooAnimal
- 接下来初始化
Panda
的第一个直接基类Bear
- 然后初始化
Panda
的第二个直接基类Endangered
- 最后初始化
Panda
自己的数据成员
继承的构造函数与多重继承
C++11允许派生类从它的一个或多个基类中继承构造函数,但如果从多个基类中继承了相同的构造函数(即形参列表完全相同),程序会产生错误
struct Base1 {
Base1() = default;
Base1(const std::string&);
Base1(std::shared_ptr<int>);
};
struct Base2 {
Base2() = default;
Base2(const std::string&);
Base2(int);
};
// 错误:两个基类的构造函数具有相同的参数列表
struct D1: public Base1, public Base2 {
using Base1::Base1; // 从Base1继承构造函数
using Base2::Base2; // 从Base2继承构造函数
};
如果一个类从它的多个基类中继承了相同的构造函数,则必须为该构造函数定义其自己的版本
struct D2: public Base1, public Base2 {
using Base1::Base1; // 从Base1继承构造函数
using Base2::Base2; // 从Base2继承构造函数
// D2必须自定义一个接受string的构造函数
D2(const string &s): Base1(s), Base2(s) { }
D2() = default; // 一旦D2定义了自己的构造函数,则必须出现
};
和单继承相同,多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行这些操作。只有当派生类使用的是合成版本的拷贝、移动或赋值成员时,才会自动处理其基类部分。在合成版本的拷贝控制成员中,每个基类分别使用自己的对应成员隐式地完成构造、赋值或销毁等工作
18.3.2 类型转换与多个基类
多重继承和单继承相同,某个可访问基类的指针或引用可以直接指向派生类对象
编译器不会在派生类向基类的几种转换中进行比较和选择
void print(const Bear&);
void print(const Endangered&);
Panda ying_yang("ying_yang");
print(ying_yang); // 二义性错误
和单继承相同,对象、指针和引用的静态类型决定了我们可以使用的成员
18.3.3 多重继承下的类作用域
- 在单继承中,派生类的作用域嵌套在直接基类和间接基类的作用域中。名称查找沿着继承体系自底向上进行,直到找到所需的名字。派生类的名字会隐藏基类的同名成员。
- 在多重继承中,相同的查找过程在所有基类中同时进行。如果名字在多个基类中都被找到,则会产生二义性错误
派生类可以从多个基类中分别继承名字相同的成员,但是在使用该名字时必须明确指出其版本
例如,ZooAnimal
和Endangered
都定义了名为max_weight
的成员,并且Panda
没有定义该成员,则下面的调用是错误的
double d = ying_yang.max_weight()
避免潜在二义性的最好方法是在派生类中定义新的版本
例如,我们可以为Panda
类定义一个max_weight
函数从而解决二义性问题
double Panda::max_weight() const {
return max(ZooAnimal::max_weight(), Endangered::max_weight());
}
18.3.4 虚继承
尽管在派生列表中同一个基类只能出现一次,但实际上派生类可以多次继承同一个类。派生类可以通过它的两个直接基类分别继承同一个间接基类,也可以直接继承某个基类,然后通过另一个基类再次间接继承该类
默认情况下,派生类含有继承链上每个类对应的子部分。如果某个类在派生过程中出现了多次,则派生类中会包含该类的多个子对象。这种默认情况对某些类并不适用。例如iostream
,它直接继承自istream
和ostream
,而istream
和ostream
都继承自base_ios
,所以iostream
继承了base_ios
两次。如果iostream
对象包含base_ios
的两份拷贝,则无法在同一个缓冲区中进行读写操作
虚继承可以让某个类共享它的基类,其中共享的基类子对象称为虚基类。在该机制下,不论虚基类在继承体系中出现了多少次,派生类都只包含唯一一个共享的虚基类子对象
通常情况下,使用虚继承的类层次是由一个人或一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个是虚基类,况且新基类的开发者也无法改变已存在的类体系
虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身
指定虚基类的方式是在派生列表中添加关键字virtual
// public和virtual的顺序随意
class Raccoon : public virtual ZooAnimal { /* ... */ };
class Bear : virtual public ZooAnimal { /* ... */ };
如果某个类指定了虚基类,则该类的派生仍按照常规方式进行
class Panda : public Bear, public Raccoon, public Endangered { /* ... */ };
不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作
因为在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,而且不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,则也可以直接访问该成员。
但如果成员被多个基类覆盖,则一般情况下派生类必须为该成员定义新的版本。例如,假设类B
定义了一个名为x
的成员,D1
和D2
都从B
虚继承得到,D
继承了D1
和D2
。则在D
的作用域中,X
通过D
的两个基类都是可见的。如果通过D
的对象使用x
,则有三种可能性:
- 如果
D1
和D2
中都没有x
的定义,则x
会被解析为B
的成员,此时不存在二义性 - 如果
D1
和D2
中的某一个定义了x
,派生类的X
会比共享虚基类B
的X
优先级更高,此时同样没有二义性 - 如果
D1
和D2
都定义了x
,则直接访问x
会产生二义性问题
18.3.5 构造函数与虚继承
在虚派生中,虚基类是由最低层的派生类初始化的。如果按普通规则处理,虚基类将会在多条继承路径上被重复初始化
继承体系中的每个类都可能在某个时刻成为“最低层的派生类”。只要能创建虚基类的派生类对象,该派生类的构造函数就必须初始化它的虚基类。即使虚基类不是派生类的直接基类,构造函数也可以进行初始化
Bear::Bear(std::string name, bool onExhibit)
: ZooAnimal(name, onExhibit, "Bear") {}
Raccoon::Raccoon(std::string name, bool onExhibit)
: ZooAnimal(name, onExhibit, "Raccoon") {}
Panda::Panda(std::string name, bool onExhibit)
: ZooAnimal(name, onExhibit, "Panda"),
Bear(name, onExhibit),
Raccoon(name, onExhibit),
Endangered(Endangered::critical),
sleeping flag(false) {}
构造含有虚基类的对象时,首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,之后再按照直接基类在派生列表中出现的顺序依次对其初始化
例如,构造Panda
对象时
- 首先使用
Panda
的构造函数初始值列表中提供的初始值构造虚基类ZooAnimal部分 - 接下来构造
Bear
部分 - 然后构造
Raccoon
部分 - 然后构造第三个直接基类
Endangered
- 最后构造
Panda
部分
虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关
一个类可以有多个虚基类,此时这些虚子对象按照它们在派生列表中出现的顺序依次构造