多重继承
多重继承是指从多个直接基类中产生派生类的能力,多重继承的派生类继承了所有父类的属性。
为了方便探讨多重继承的问题,我们以动物园的动物层次为例,UML类图如下:
点击查看【processon】
在代码上的体现如下:
class Bear : public ZooAnimal { /*. . . */ };
class Panda : public Bear, public Endangered { /*. . . */ };
继承所有基类状态
构造顺序
在Panda构造函数中,初始化Panda的所有直接基类,初始化的顺序与构造函数初始值列表无关,只与出现在派生列表中的先后顺序相一致。
//显式地初始化所有基类
Panda::Panda(std::string name, bool onExhibit)
:Endangered(Endangered::critical))
,Bear(name, onExhibit, "Panda"{
//即使Endangered基类在第一个,依然是先执行Bear构造。
//最终的构造顺序是:
//1、ZooAnimal()
//2、Bear()
//3、Endangered()
//4、Panda()
}
Panda::Panda()
//隐式地使用Bear的默认构造函数初始化Bear子对象
:Endangered(Endangered::critical){
//构造顺序同上。
}
继承构造函数
如果派生类继承的多个基类之间有相同参数列表的构造函数,则派生类必须重定义该构造函数,否则报错。
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; //从 Basel 继承构造函数
using Base2::Base2; //从 Base2 继承构造函数
//在D1的基类中有相同的形参列表的构造函数,D1必须重定义该构造函数,否则报错
//D1必须自定义一个接受 string 的构造函数
D1(const string &s)
:Base1(s)
,Base2(s){
}
D1() = default; //一旦D2定义了它自己的构造函数,则必须出现
} ;
析构顺序
析构函数的调用顺序和构造函数刚好相反,即:
Panda::~Panda(){
//1、Panda()
//2、Endangered()
//3、Bear()
//4、ZooAnimal()
}
拷贝与移动操作
如果派生类定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行拷贝、移动或赋值操作。只有当派生类使用的是合成版本的拷贝、移动或赋值成员时,才会自动对其基类部分执行这些操作。在合成的拷贝控制成员中,每个基类分别使用自己的对应成员隐式地完成构造、赋值或销毁等工作。
Panda fucker1;
Panda fucker2 = fucker1; //执行合成的拷贝构造函数。
//1、ZooAnimal的拷贝构造。
//2、Bear的拷贝构造。
//3、Endangered的拷贝构造。
//4、Panda的拷贝构造。
类型转换
派生类的指针、引用可以自动转成所有基类(包括间接基类)的指针引用。
编译器不会在派生类向基类的几种转换中进行比较和选择,因为在它看来转换到任意一种基类都一样好。
//接受Panda的基类引用的一系列操作
void print(const Bear&);
void highlight(const Endangered&);
ostream& operator<< (ostream&, const ZooAnimal&);
Panda fucker("shit");
print(fucker); //把一个Panda对象传递给一个Bear的引用
highlight(fucker); //把一个Panda对象传递给一个Endangered的引用
cout << fucker << endl; //把一个Panda对象传递给一个ZooAnimal的引用
void bitch(const Bear&);
void bitch(const Endangered&);
bitch(fucker); //二义性错误,两个都一样好。
但是对象的指针、引用的静态类型决定了可以使用那些成员。比如Bear的指针,只能调用Bear中可以调用的成员。
名字查找
在单继承中,派生类的作用域嵌套在基类和间接基类中。名字查找过程是自下向上的过程,基类的成员将被派生类的同名成员隐藏。
在多重继承中,遵循相同的查找过程,但是在每条继承链上时并行进行的,即在Endangered和Bear/ZooAnimal两条链上。只要这两条链出现相同成员,就有二义性,即使名字对应函数的参数列表不同,也会发生错误(先查找,再匹配),即使在一个是public,一个是private也可能产生错误。
最好的办法,当然是自定义,或者是使用::作用域运算符显式指定作用域。
虚继承
派生类只能直接继承基类一次,但是可以间接继承基类多次。
默认地,每一个继承都会包含一个该基类的对象,比如
点击查看【processon】
Panda又继承Raccoon(浣熊科,科学界对Panda的所属依然有争议)。那此时Panda将拥有两份ZooAnimal对象的内容!
虚继承的作用,就是让Panda只保留一份ZooAnimal对象,做法如下:
点击查看【processon】
代码写法:
//关键字public和virtual的顺序随意
class Raccoon : public virtual ZooAnimal { ... };
class Bear : public virtual ZooAnimal { ... };
//ZooAnimal是Raccoon和Bear的虚基类。
virtual更多像是一种预防,保证虚基类在后续的派生类中只有一份实例。除此之外,虚继承并不会给类带来其他影响。Panda的继承方式并没有什么改变。
虚基类成员的可见性
点击查看【processon】
继承关系如上:
假设B中存在x成员。
- 若D1、D2都没有x成员,则在ShitOnMe中访问的就是虚基类B的x成员。
- 若D1、D2其中之一有,则在ShitOnMe中访问的是D1/D2的x
- 若D1、D2都有x,则产生二义性。
构造顺序
在虚派生中,虚基类是由最低层的派生类初始化的。也就是说在设计类时就当考虑是否存在虚继承问题,是否需要直接初始化虚基类。由Panda的构造函数中完成ZooAnimal的构造初始化。
Panda::Panda(std::string name, bool onExhibit)
:ZooAnimal(name, onExhibit, " Panda"),
,Bear(name, onExhibit)
,Raccoon(name, onExhibit)
,Endangered(Endangered::critical){
//构造顺序和前面的略有不同,首先必须是虚基类的构造
//然后就是上面的所讲的顺序。
//1、ZooAnimal()
//2、Bear()
//3、Raccoon()
//4、Endangered()
//5、Panda()
}
Panda::Panda(std::string name, bool onExhibit)
//隐式调用虚基类的默认构造函数
:Bear(name, onExhibit)
,Raccoon(name, onExhibit)
,Endangered(Endangered::critical){
//1、ZooAnimal()默认构造函数,如果没有则报错。
//2、Bear()
//3、Raccoon()
//4、Endangered()
//5、Panda()
}
如果存在多个虚基类,则按照在派生列表中的出现顺序从左到右依次构造。
class C{};
class B : public C{};
class ToyAnimal{}
class Fuck : public B, public Bear, public virtual ToyAnimal
//构造顺序
//1、ZooAnimal()
//2、ToyAnimal()
//3、C()
//4、B()
//5、Bear()
//6、Fuck()
析构顺序
与虚继承的构造顺序刚好相反。