🥒多重继承与虚继承
2022-6-10
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property

 
C++ 是可以多重继承的(Multiple inheritance),意味着一个派生类可以多个直接基类。多重派生类(multiply derived class)继承其所有的父类的属性。尽管在概念上很简单,但是在细节上涉及到多个直接基类的时候会在设计层面和实现层面遇到许多问题。
                                           单继承
单继承
                                                             多继承
多继承

多重继承

多重继承的派生列表会包含多个基类:
每个基类都有一个可选的访问说明符,如果省略的话就提供默认的说明符,对于classprivate,对于structpublic。与单一继承一样,派生列表中的类必须是已经定义的,并且不能是final的。语言并没有限制具体可以在派生列表中包含多少个类。每个基类只能在派生列表中出现一次。
多重派生类从每个基类中继承状态,一个派生类对象将包含所有基类的子对象。Panda类中包含了BearEndangered子对象,以及它自身定义的成员。
 
构建派生类对象需要构建和初始化其所有的直接基类子对象:
构造函数的初始化列表负责初始化所有的直接基类,可将参数传递个直接基类的构造函数作为实参。直接基类的初始化顺序它们出现在派生列表中的顺序。
 
继承构造函数和多重继承
在新标准下,可以用using声明的方式从一个或多个基类中继承构造函数,如果从多个基类中继承具有相同的签名的构造函数将导致编译错误。如果发生了这样的情况需要派生类重新定义此构造函数:
 
析构函数和多重继承
多重继承的析构函数与单一继承的析构函数没有什么不同。析构函数本身只需要负责它自己的资源的释放,成员和基类的资源都由他们各自释放。如果没有定义析构函数,编译器会自动合成一个,其函数体依然是空的。析构的顺序与构造的顺序刚好是完全相反的,所以将先调用成员的析构函数(属于最底层的派生类),再依次以派生列表的相反顺序调用基类的析构函数。
 
多重派生类的拷贝和移动操作
与单继承一样,如果多重继承的子类如果要定义自己的拷贝、移动构造函数以及赋值操作符将必须拷贝、移动或赋值整个对象。只用派生类使用这些成员的合成版本时,基类才会自动进行拷贝、移动或赋值。在派生类的合成版本中会自动隐式使用基类的对应成员。
 

转换和多基类

在多重继承下任何可访问的基类的指针或引用都可以绑定到派生对象上。事实上,编译器认为所有以上的转换是同样好的,意味着如下代码将是编译错误:
基于指针或引用类型的查找
在多重继承中,对象、指针、引用的静态类型决定了使用哪个成员,即便是指向子类对象,其其它的基类的接口或者子类自己的接口亦是不可用的。
 
 

钻石继承

钻石继承,又称菱形继承(diamond-inheritance),是多继承的一种特殊情况
notion image
notion image
钻石继承存在数据冗余和二义性的问题:
二义性通过指定作用域是可以解决的,告诉编译器是从学生那继承的还是从老师那继承的。但是数据冗余没有解决,数据冗余带来的最大问题是空间的浪费:
 

多重继承下的类作用域

  • 在单一继承下,派生类的作用域被嵌套在直接和间接基类中。名字的查找将沿着继承链一直往上,定义在派生类中的名字将屏蔽掉基类中的名字。
  • 在多重继承中,名称查找将同时在所有直接基类中查找,如果一个名字在多个基类中找到就被认为是具有二义性。即便是两个名字所代表的函数的函数原型不一样也是错误的,甚至两个名字其中一个不是函数也会产生二义性错误。与往常一样,名称查找发生在类型检查之前。
多重继承中继承相同名字是可以的,但是如果想要引用其中一个名字则需要指定哪个版本。最好的办法是在派生类中为这些可能产生二义性的名字重新定义一个函数。
 

虚继承

对于空间浪费,有什么解决的方法吗?有的,使用虚拟继承去解决钻石继承的数据冗余问题。在类腰部位置加一个virtual关键字:
再配合指定作用域,二义性也可以得到很好的解决。
 
一个类可能继承一个相同的基类多次,原因在于某些基类都继承自同一个基类。这种情况下将导致同一个基类有两个子对象。但是有时需要让这个相同的基类只有一个子对象。通过虚继承(virtual inheritance)来解决此问题,共享的基类子对象成为虚基类(virtual base class),不管这个虚基类在继承链中出现了多少次,只有一个共享子对象:
此处ZooAnimal是虚基类,virtual告知愿意在接下来的继承中共享同一个基类对象,对于所使用的基类本身并没有什么特别的限制。
notion image
Panda对象中就只有一个ZooAnimal子对象了。
 
支持到基类的转换,一个基类是不是虚基类都可以用其指针或引用指向派生类对象。
虚基类成员的可见性:如果虚基类中的成员被其中一个路径上的派生子类对象覆盖而不是被所有路径上的派生子类对象覆盖的话,那么引用这个成员将是派生对象上的成员,如果所有路径都覆盖的话,那么就会产生二义性错误。这时最好的做法就是在底层的派生类对象中重新定义此成员。
 

构造函数和虚继承

在虚继承中,虚基类是由最后面的派生构造函数进行初始化,否则的话虚基类就可能在所有路径中被初始化,从而导致初始化多次。当继承体系中派生类如果可以独立被创建的话,它的构造函数也是最后面的派生构造函数,此时它也需要对虚基类进行初始化。在这种情况下将由最底层的派生类构造函数首先对虚基类子对象进行初始化,后面遇到的初始化虚基类子对象将会被忽略。然后再按照正常的派生列表顺序进行初始化其它基类。
虚基类总是在非虚基类前被初始化,而不管它们出现在继承层级的哪个位置。
 
构造和析构顺序
如果一个类有多个虚基类,那么其顺序将按照出现在派生列表中的顺序进行初始化。
将按照如下顺序进行初始化:
对于拷贝和移动构造函数来说其顺序是一样的,合成的赋值操作符则是按照此顺序进行赋值的。而析构函数则以此反方向执行。
 
  • C++
  • 构造函数与拷贝控制有关继承的思考
    目录