第9章 多重继承

多重继承(MI)的基本概念听起来相当简单:通过继承多个基类来创建一个新类。确切地说这种多重继承语法正是我们所期望的,并且只要继承层次结构图是简单的,那么多重继承也同样简单。

尽管MI可能引入一些二义性和奇怪的案例,在本章将对这些案例进行讨论。但是首先,这些案例将有助于读者对该主题获得一些基本认识。

9.1 概论

在C++之前,最成功的面向对象的语言是Smalltalk。Smalltalk是作为一种完全的面向对象语言而创造出来的。它被称作是纯粹的(pure)面向对象语言,而C++则被称作是一种混合的(hybrid)语言,这是因为C++支持多种形式的程序设计范例,而不仅仅只是面向对象的程序设计范例。一个由Smalltalk做出的设计本身就决定了所有类都是在一个单一的继承层次结构中派生的,都以一个基类作为根(称为Object—这就是基于对象的继承层次结构(object-based hierarchy)模型)[1]。在Smalltalk中,不可能创建这样一个新类:它不是派生自一个现存的类。这就是为什么在Smalltalk中实现多种形式的继承方式要花费大量的时间:在开始建立新类前,必须学习和掌握类库。因此Smalltalk的类继承层次结构是一棵单一的整体树。

Smalltalk中的类通常有很多的共同点,并且总是有某些共通的东西(Object的特征和行为),所以不会经常遇上需要从多个基类继承的情况。然而,在C++中却可以建立用户想要的多种不同的继承树。所以为了逻辑上的完整性,该语言必须有能力一次组合多个类—因而需要多重继承。

然而,程序员对多重继承的需求并不是显而易见的。关于在C++中多重继承是否是必要的这一问题存在着(现在仍然存在着)大量的争论。1989年在AT&T cfront发布版(release)2.0中加入了MI,这也是C++语言1.0版以来发生的首次重要的变化。[2]从那以后,许多其他的特征被加入标准C++中(最著名的是模板),这些变化改变了编程的思想并且使MI的作用处于次要的地位。程序设计人员可以把MI看做是一个“次要”的语言特征,也就是说,在日常的程序设计决定中很少涉及它。

有关MI最激烈的争论之一涉及容器。假如要想建立这样一个容器,每个人都可以很容易地使用它。一种方法是将void*作为该容器内部的类型。然而Smalltalk的方法是建立一个持有Object对象的容器,因为Object是Smalltalk继承层次结构的基类型。Smalltalk中的所有内容最终都派生自Object,所以持有Object的容器可以存储任何类型的对象。

现在考虑在C++中的情况。假设供应商A建立了一个基于对象的继承层次结构,该继承层次结构包括了一组有用的容器,这些容器中就包含想要使用的一种称为Holder的容器。接下来偶然遇到了供应商B提供的类继承层次结构,它包含了其他一些比较重要的类,例如BitImage类,它持有生动的图像。制造一个持有这些BitImage特征和行为的Holder容器的惟一方法,是创建一个派生自Object和BitImage两者的新类,这样,在Holder中就可以持有BitImage中的特征和行为:

第9章 多重继承 - 图1

这似乎是需要MI的一个重要理由,而许多类库就是建立在这种模型之上的。然而如第5章所述,模板的加入改变了创建容器的方法。所以这种情况不再是使用MI的动力。

而需要MI的另外一个原因跟设计有关。可以有意地用MI来使一个程序设计得更加灵活或更实用(至少表面上是这样)。在原始的iostream库设计中就有这样的一个例子(仍存在于目前的模板设计中,如第4章所述):

第9章 多重继承 - 图2

istream和ostream就其自身来说都是有用的类,但是也可以通过从一个类同时派生出这两个类的方式产生它们,而该基类将这两个类的特征和行为结合在一起。类ios提供了所有这些流类的共同点,在这种情况下MI就是一种代码分解机制。

不管是什么原因激发我们使用MI,但是要真正使用它将比看上去要难得多。

[1]对Java和其他面向对象的语言来说这也是正确的。

[2]这些版本号是国际AT&T的编号方式。