多重继承定义
多重继承,multiple inheritance,简称MI。在现实中,也许某种事物具有两种或以上的种类的事物的基础属性,所以C++中有多重继承这种手段来解决这种问题。即允许一个派生类指定多个基类。例如下面这个类,从侍者类公有继承,从歌手类私有继承,得到一个“会唱歌的侍者”类:
1 | class singingwaiter : public waiter, singer //singer is private |
必须为每个类添加限定符,如果没有,默认是私有继承。
MI会给程序员带来很多编程问题。因此许多人反对MI,甚至希望删除MI;但对于喜欢MI的人来说,MI很有用,用得好能大大缩减代码量,甚至对于特殊工程来说MI是必不可少的;也有一些人建议谨慎使用MI。主要的两个问题是:从两个不同的基类继承同名方法;从两个或更多的相关基类那里继承同一个类的多个实例。
多重继承可能存在的问题
继承同名类方法的二义性错误解决
水陆两用汽车(amphibiancar)具有小汽车和小船两种属性,所以由小汽车类和小船类共同派生出一个水陆两用汽车类。那么假设这么一个问题,交通工具就认为它是抽象基类吧,有一个虚函数show()
,大概就是显示一些信息之类的,小汽车当然就继承了car::show()
,小船则是boat::show()
。
那么面对两个同名类方法,并且在这里假如在水陆两用汽车类中没有重新定义该方法,那到底继承的是什么呢?或者说,在水陆两用汽车类中调用AmphibianCar::show()
,那么这明显是有问题的有二义性错误(ambiguous) 。当然,也有两个setweight()
的错误
有以下解决方法:比如使用作用域解析运算符直接调用,如果有一个amphibiancar
对象a
,那么调用a.car::show();
。更好的方法就是编写成员函数,直接指出使用哪个show
,或者重新定义show
1
2
3
4void amphibiancar:: show()
{
car::show();
}
继承多个同名基类对象,需要用到虚基类
因为抽象基类:交通工具有一个weight
,小汽车和小船都从交通工具那儿各自继承了一个weight
组件,因此水陆两用汽车将会包含两个weight组件,这会引发一系列问题。
例如常见的派生类对象(的地址)可以赋给基类(指针)引用,但现在将出现二义性。原理是这种赋值是把基类指针设置为派生类对象中基类对象的地址。但现在派生类含有两个weight
对象,所以:1
2
3
4
5amphibiancar(...) a;
vehicle * p1 =
vehicle * p2 = (car *)
vehicle * p3 = (boat *)
是不是很蛋疼?说明这种同名对象的继承会影响多态性的复杂。实际上这里的水陆两用汽车不需要两个weight成员,(虽说有的时候还真的可以继承多个同名成员)所以真正的问题是我们只需要一个变量来描述重量就可以了。C++引入了虚基类(virtual base class)来解决问题。
虚基类语法
虚基类使得从多个类(它们基类相同)派生出来的类,只继承一个基类对象。在类声明的派生中添加关键字virtual
。virtua
l和public
的顺序不关紧要。1
2class Car : virtual public Vehicle ;
class Boat : virtual public Vehicle ;
如果这样做,那么amphibiancar
类将只包含vehicle
对象的一个副本,从本质上来说,继承car
和boat
类共享一个vehicle
对象,而不是各自引入一个。
新的构造函数规则
用到虚基类时,需要对类构造函数使用一种新的方法。对于非虚基类,出现在初始化列表的构造函数 是 即时基类的构造函数。例如:
1 | class A |
C
类的构造函数只能调用B
类的构造函数,B
类的构造函数只能调用A
类的构造函数。c
的构造函数使用值q
,并将m
和n
传递给B
的构造函数,B
使用m
,将n
传递给A
的构造函数。
如果vehicle是虚基类,这种自动传递信息则不再起作用,例如在上面的多重继承条件下有这样的构造函数:
1 | amphibiancar(const vehicle & v, int air = 0, int ton = 0)::car(v,air),boat(v,ton) {}//failed |
会出错。原因是:自动传递信息时,将通过两条不同的途径(car or boat)将v
传递给vehicle
。为避免这种冲突,C++在基类是虚基类时,禁止信息通过中间类自动传递给基类。然而,编译器仍然需要保证派生类对象生成之前先调用基类构造函数组件,因此需要显式调用(基类构造函数),因此正确的构造函数应该是这样:
1 | amphibiancar(const vehicle & v, int air = 0, int ton = 0)::vehicle(v),car(v,air),boat(v,ton) {} |
上面代码显示调用构造函数vehicle(const vehicle &)
。请注意!这种用法是合法的,而且对于虚基类来说,必须这样做。但对于非虚基类,这是非法的。
MI小结
- 不使用虚基类的MI:这种方式不会引入新的规则。例如:
class Student :private std::string, private std::valarray<double>
如果一个类从两个不同的类哪里继承了两个同名的成员(通常原因是有共同祖先啦)则需要额外做一些编程工作。如果是同名方法,需要使用作用域解析运算符来说明到底是使用哪一个方法,或者重新编写方法。否则 会有二义性错误。
如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径分别继承非虚基类的一个实例。
当派生类使用关键字virtual来指示派生时,基类成为虚基类:
class Car : virtual public Vehicle{...};//vehicle成为虚基类
主要变化(同时也是使用虚基类的原因)是:从虚基类的一个或多个实例派生而来的类将只继承一个基类对象。为了实现这种特性,需要做的其他工作有:
• 有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数。这对间接非虚基类来说是非法的。
• 优先规则解决名称二义性
最后总结,综上所述,MI会增加编程复杂程度。主要是由于派生类通过多种途径继承了同一个基类所引起的。
参考链接
-多重继承