C++学习之多重继承

多重继承定义

多重继承,multiple inheritance,简称MI。在现实中,也许某种事物具有两种或以上的种类的事物的基础属性,所以C++中有多重继承这种手段来解决这种问题。即允许一个派生类指定多个基类。例如下面这个类,从侍者类公有继承,从歌手类私有继承,得到一个“会唱歌的侍者”类:

1
2
3
4
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
4
void amphibiancar:: show()
{
car::show();
}

继承多个同名基类对象,需要用到虚基类

因为抽象基类:交通工具有一个weight,小汽车和小船都从交通工具那儿各自继承了一个weight组件,因此水陆两用汽车将会包含两个weight组件,这会引发一系列问题。

例如常见的派生类对象(的地址)可以赋给基类(指针)引用,但现在将出现二义性。原理是这种赋值是把基类指针设置为派生类对象中基类对象的地址。但现在派生类含有两个weight对象,所以:

1
2
3
4
5
amphibiancar(...) a;
vehicle * p1 = & a; //这里出现二义性错误

vehicle * p2 = (car *) & a;
vehicle * p3 = (boat *) & a;

是不是很蛋疼?说明这种同名对象的继承会影响多态性的复杂。实际上这里的水陆两用汽车不需要两个weight成员,(虽说有的时候还真的可以继承多个同名成员)所以真正的问题是我们只需要一个变量来描述重量就可以了。C++引入了虚基类(virtual base class)来解决问题。

虚基类语法

虚基类使得从多个类(它们基类相同)派生出来的类,只继承一个基类对象。在类声明的派生中添加关键字virtualvirtual和public的顺序不关紧要。

1
2
class Car : virtual public Vehicle{...};
class Boat : virtual public Vehicle{...};

如果这样做,那么amphibiancar类将只包含vehicle对象的一个副本,从本质上来说,继承carboat类共享一个vehicle对象,而不是各自引入一个。

新的构造函数规则

用到虚基类时,需要对类构造函数使用一种新的方法。对于非虚基类,出现在初始化列表的构造函数 是 即时基类的构造函数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A
{
int a;
public:
A(int n = 0): a(n){}
};

class B :public A
{
int b;
public:
B(int m = 0, int n = 0):A(n) , b(m) {}

};

class C :public B
{
int c;
public:
c(int q = 0, int m = 0, int n = 0): B(m, n), c(q){}
};

C类的构造函数只能调用B类的构造函数,B类的构造函数只能调用A类的构造函数。c的构造函数使用值q,并将mn传递给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会增加编程复杂程度。主要是由于派生类通过多种途径继承了同一个基类所引起的。


参考链接

-多重继承