多重继承

多重继承是指从多个直接基类中产生派生类的能力,多重继承的派生类继承了所有父类的属性。
为了方便探讨多重继承的问题,我们以动物园的动物层次为例,UML类图如下:
点击查看【processon】
在代码上的体现如下:

  1. class Bear : public ZooAnimal { /*. . . */ };
  2. class Panda : public Bear, public Endangered { /*. . . */ };

继承所有基类状态

在多重继承关系中,派生类对象包含派生链上所有基类的对象。
image.png

构造顺序

在Panda构造函数中,初始化Panda的所有直接基类,初始化的顺序与构造函数初始值列表无关,只与出现在派生列表中的先后顺序相一致。

  1. //显式地初始化所有基类
  2. Panda::Panda(std::string name, bool onExhibit)
  3. :Endangered(Endangered::critical))
  4. ,Bear(name, onExhibit, "Panda"{
  5. //即使Endangered基类在第一个,依然是先执行Bear构造。
  6. //最终的构造顺序是:
  7. //1、ZooAnimal()
  8. //2、Bear()
  9. //3、Endangered()
  10. //4、Panda()
  11. }
  12. Panda::Panda()
  13. //隐式地使用Bear的默认构造函数初始化Bear子对象
  14. :Endangered(Endangered::critical){
  15. //构造顺序同上。
  16. }

继承构造函数

如果派生类继承的多个基类之间有相同参数列表的构造函数,则派生类必须重定义该构造函数,否则报错。

  1. struct Base1 {
  2. Base1() = default;
  3. Base1(const std::string&);
  4. Base1(std::shared_ptr<int>);
  5. };
  6. struct Base2 {
  7. Base2() = default;
  8. Base2(const std::string&);
  9. Base2(int);
  10. };
  11. struct D1: public Base1, public Base2{
  12. using Base1::Base1; //从 Basel 继承构造函数
  13. using Base2::Base2; //从 Base2 继承构造函数
  14. //在D1的基类中有相同的形参列表的构造函数,D1必须重定义该构造函数,否则报错
  15. //D1必须自定义一个接受 string 的构造函数
  16. D1(const string &s)
  17. :Base1(s)
  18. ,Base2(s){
  19. }
  20. D1() = default; //一旦D2定义了它自己的构造函数,则必须出现
  21. } ;

析构顺序

析构函数的调用顺序和构造函数刚好相反,即:

  1. Panda::~Panda(){
  2. //1、Panda()
  3. //2、Endangered()
  4. //3、Bear()
  5. //4、ZooAnimal()
  6. }

拷贝与移动操作

如果派生类定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行拷贝、移动或赋值操作。只有当派生类使用的是合成版本的拷贝、移动或赋值成员时,才会自动对其基类部分执行这些操作。在合成的拷贝控制成员中,每个基类分别使用自己的对应成员隐式地完成构造、赋值或销毁等工作。

  1. Panda fucker1;
  2. Panda fucker2 = fucker1; //执行合成的拷贝构造函数。
  3. //1、ZooAnimal的拷贝构造。
  4. //2、Bear的拷贝构造。
  5. //3、Endangered的拷贝构造。
  6. //4、Panda的拷贝构造。

类型转换

派生类的指针、引用可以自动转成所有基类(包括间接基类)的指针引用。
编译器不会在派生类向基类的几种转换中进行比较和选择,因为在它看来转换到任意一种基类都一样好。

  1. //接受Panda的基类引用的一系列操作
  2. void print(const Bear&);
  3. void highlight(const Endangered&);
  4. ostream& operator<< (ostream&, const ZooAnimal&);
  5. Panda fucker("shit");
  6. print(fucker); //把一个Panda对象传递给一个Bear的引用
  7. highlight(fucker); //把一个Panda对象传递给一个Endangered的引用
  8. cout << fucker << endl; //把一个Panda对象传递给一个ZooAnimal的引用
  9. void bitch(const Bear&);
  10. void bitch(const Endangered&);
  11. bitch(fucker); //二义性错误,两个都一样好。

但是对象的指针、引用的静态类型决定了可以使用那些成员。比如Bear的指针,只能调用Bear中可以调用的成员。

名字查找

在单继承中,派生类的作用域嵌套在基类和间接基类中。名字查找过程是自下向上的过程,基类的成员将被派生类的同名成员隐藏。
在多重继承中,遵循相同的查找过程,但是在每条继承链上时并行进行的,即在Endangered和Bear/ZooAnimal两条链上。只要这两条链出现相同成员,就有二义性,即使名字对应函数的参数列表不同,也会发生错误(先查找,再匹配),即使在一个是public,一个是private也可能产生错误。
最好的办法,当然是自定义,或者是使用::作用域运算符显式指定作用域。

虚继承

派生类只能直接继承基类一次,但是可以间接继承基类多次。
默认地,每一个继承都会包含一个该基类的对象,比如
点击查看【processon】
Panda又继承Raccoon(浣熊科,科学界对Panda的所属依然有争议)。那此时Panda将拥有两份ZooAnimal对象的内容!
虚继承的作用,就是让Panda只保留一份ZooAnimal对象,做法如下:
点击查看【processon】
代码写法:

  1. //关键字public和virtual的顺序随意
  2. class Raccoon : public virtual ZooAnimal { ... };
  3. class Bear : public virtual ZooAnimal { ... };
  4. //ZooAnimal是Raccoon和Bear的虚基类。

virtual更多像是一种预防,保证虚基类在后续的派生类中只有一份实例。除此之外,虚继承并不会给类带来其他影响。Panda的继承方式并没有什么改变。

虚基类成员的可见性

点击查看【processon】
继承关系如上:
假设B中存在x成员。

  • 若D1、D2都没有x成员,则在ShitOnMe中访问的就是虚基类B的x成员。
  • 若D1、D2其中之一有,则在ShitOnMe中访问的是D1/D2的x
  • 若D1、D2都有x,则产生二义性。

最好的做法,就是ShitOnMe自己定义x。

构造顺序

在虚派生中,虚基类是由最低层的派生类初始化的。也就是说在设计类时就当考虑是否存在虚继承问题,是否需要直接初始化虚基类。由Panda的构造函数中完成ZooAnimal的构造初始化。

  1. Panda::Panda(std::string name, bool onExhibit)
  2. :ZooAnimal(name, onExhibit, " Panda"),
  3. ,Bear(name, onExhibit)
  4. ,Raccoon(name, onExhibit)
  5. ,Endangered(Endangered::critical){
  6. //构造顺序和前面的略有不同,首先必须是虚基类的构造
  7. //然后就是上面的所讲的顺序。
  8. //1、ZooAnimal()
  9. //2、Bear()
  10. //3、Raccoon()
  11. //4、Endangered()
  12. //5、Panda()
  13. }
  14. Panda::Panda(std::string name, bool onExhibit)
  15. //隐式调用虚基类的默认构造函数
  16. :Bear(name, onExhibit)
  17. ,Raccoon(name, onExhibit)
  18. ,Endangered(Endangered::critical){
  19. //1、ZooAnimal()默认构造函数,如果没有则报错。
  20. //2、Bear()
  21. //3、Raccoon()
  22. //4、Endangered()
  23. //5、Panda()
  24. }

如果存在多个虚基类,则按照在派生列表中的出现顺序从左到右依次构造。

  1. class C{};
  2. class B : public C{};
  3. class ToyAnimal{}
  4. class Fuck : public B, public Bear, public virtual ToyAnimal
  5. //构造顺序
  6. //1、ZooAnimal()
  7. //2、ToyAnimal()
  8. //3、C()
  9. //4、B()
  10. //5、Bear()
  11. //6、Fuck()

合成的拷贝、移动构造按照完全相同的顺序执行。

析构顺序

与虚继承的构造顺序刚好相反。