9.3 实现继承

如前所述,C++仅仅提供了实现继承,这就意味着所有的内容总是继承自基类。这样做有很大的好处,因为它将使程序员从不得不在派生类中实现所有的细节(正如前面的例子中所采用接口继承所做的事情)中解放出来。多重继承的一个共同用途包括使用混入类(mixin),这些混入类的存在是为了通过继承来增加其他类的功能。混入类不能刻意地由它本身进行实例化。

举个例子,假设一个客户使用了某个类,该类支持访问一个数据库。在这个情况下,仅仅有一个头文件可以使用—在这里指出,客户不能访问实现具体功能的这部分源代码。举例说明,假定Database类的实现如下所示:

9.3 实现继承 - 图1

这里已经省略了实际的数据库功能(存储操作、检索操作,等等),但是在这里那些功能并不重要。使用这个类需要一个数据库连接串,并调用Database:open()来连接数据库,通过调用Database:close()断开连接:

9.3 实现继承 - 图2

在一个典型的客户机-服务器模式的情况下,客户拥有多个对象,这些对象分享一个连接的数据库。尽管数据库的最后关闭是非常重要的,但数据库只能在不再需要访问它之后关闭。通常,将这种行为封装到一个类中,用来实现对使用数据库连接的客户实体的数目进行跟踪,并且在实体计数归为零时自动终止数据库的连接。为了给Database类加入引用计数,利用多重继承将一个叫Countable的类混入Database类中,这样就创建了一个新类DBConnection。这就是Countable混入类:

9.3 实现继承 - 图3

很明显,这不是一个独立类,因为它的构造函数是protected类型;它需要一个友元或派生类来使用它。析构函数是虚函数这一点非常重要,因为它只被detach()中的delete this语句调用,并且需要将派生对象正确地销毁。[1]

DBConnection类继承了Database和Countable,并且提供了一个静态的create()函数,这个函数用来初始化它的Countable子对象。这是将在第10章中讨论的工厂方法(Factory Method)设计模式的一个例子:

9.3 实现继承 - 图4

9.3 实现继承 - 图5

不用修改Database类,现在就有一个引用计数的数据库连接,并且可以确保数据库连接不会被偷偷地终止。通过DBConnection的构造函数和析构函数,使用第1章中提到的资源获取式初始化(the Resource Acquisition Is Initialization, RAII)方法来实现数据库的打开和关闭。这就使得DBConnection的使用变得很容易:

9.3 实现继承 - 图6

因为对DBConnection:create()的调用又调用了attach(),所以在结束时,必须显式调用detach()来释放数据库的初始连接。注意,DBClient类也用RAII管理连接的使用。当程序结束时,这两个DBClient对象的析构函数将分别使引用计数减1(通过调用detach()完成,这里的DBConnection继承自Countable),在对象c1被销毁后,当引用计数达到零时数据库连接将被关闭(因为调用了Countable的虚析构函数)。

模板方法一般用于混入继承,允许用户在编译时指定想要的混入类的类型。这样就可以使用不同的引用计数方法来完成这项工作,而不用显式地两次定义DBConnection。下面这个例子说明了这种方法是如何工作的:

9.3 实现继承 - 图7

9.3 实现继承 - 图8

这里惟一的变化是用于类定义的模板前缀(以及为了清楚起见而将Countable重新命名为Counter)。也可以把某个数据库类作为一个模板参数(可以从多个数据库访问类中进行选择),但它并不是一个混入类,因为它是一个独立类。尽管下面的例子将原始的Countable作为Counter混入类型使用,但是可以使用实现了适当的接口(attach()、detach()等等)的任何类型。

9.3 实现继承 - 图9

多参数混入类型的一般模式很简单:

9.3 实现继承 - 图10

[1]尽管这很重要,但是我们不需要未定义的行为。对一个基类来说没有一个虚析构函数将是一个错误。