多重继承
多重继承是指从多个直接基类中产生派生类的能力,多重继承的派生类继承了所有父类的属性。
为了方便探讨多重继承的问题,我们以动物园的动物层次为例,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()
析构顺序
与虚继承的构造顺序刚好相反。
