下载
第1 5 章 模板和包容器类
包容器类常用于创建面向对象程序的构造模块 (building block),它使得程序内部代码更容易
构造。
一个包容器类可描述为容纳其他对象的对象。可把它想像成允许向它存储对象,而以后可
以从中取出这些对象的高速暂存存储器或智能存储块。
包容器类非常重要,曾被认为是早期的面向对象语言的基础。例如,在 S m a l l t a l k中,程序
员把语言设想为带有类库的程序翻译器,而类库的重要部分就是包容器类。所以 C + +编译器的
供应商很自然地会为用户提供包容器类库。
像许多早期的别的 C + +库一样,早期的包容器类库仿效 S m a l l t a l k的基于对象的层次结构,
该结构非常适合S m a l l t a l k,但是该结构在C + +的使用中却带来了一些不便,因此有必要寻求另
外的方法。
包容器类是解决不同类型的代码重用问题的另一种方法。继承和组合为对象代码的重用提
供一种方法,而C + +的模板特性为源代码的重用提供一种方法。
虽然C + +模板是通用的编程工具,但当它们被引入该语言时它们似乎不支持基于对象的包
容器类层次结构。新近版本的包容器类库则完全由模板构造,程序员可以很容易地使用。
本章首先介绍包容器类和采用模板实现包容器类的方法,接着给出一些包容器类和怎样使
用它们的例子。
15.1 包容器和循环子
假若打算用C语言创建一个堆栈,我们需要构造一个数据结构和一些相关函数,而在 C + +
中,则把二者封装在一个抽象数据类型内。下面的 s t a c k类是一个栈类的例子,为简化起见,它
仅处理整数:
296 C + +编程思想
下载
类i s t a c k是最为常见的自顶向下式的堆栈的例子。为了简化,此处栈的尺寸是固定的,但
是也可以对其修改,通过把存储器安排在堆中分配内存,来自动地扩展其长度(后面的例子会
介绍)。
第二个类i s t a c k I t e r 是循环子的例子,我们可以把其当作仅能和 i s t a c k协同工作的超指针。
注意,i s t a c k I t e r是i s t a c k的友元,它能访问i s t a c k的所有私有成员。
像一个指针一样,i s t a c k I t e r 的工作是扫视i s t a c k并能在其中取值。在上述的简单的例子中,
i s t a c k I t e r可以向前移动(利用运算符+ +的前缀和后缀形式)和取值。然而,此处却并没有对定
义循环子方法予以限制。完全可以允许循环子在相关包容器中以任何方法移动和对包容的值进
行修改。可是,按照惯例,循环子是由构造函数创建的,它只与一个包容器对象相连,并且在
生命周期中不重新相连。(大多数循环子较小,所以我们可以容易地创建其他循环子。)
为了使例子更有趣,这个 f i b o n a c c i函数产生传统的“兔子繁殖数”,这是一个相当有效的
实现,因为它决不会多次产生这些数。
在主程序m a i n ( )中,我们可以看到栈和它的相关循环子的创建和使用。一旦创建了这些类,
便可以很方便的使用它们。
包容器的必要性
很明显,一个整数堆栈不是一个重要的工具。包容器类的真正的需求是在堆上使用 n e w创
建对象和使用d e l e t e析构对象的时候体现的。一个普遍的程序设计问题是程序员在写程序时不
知道将创建多少对象。例如在设计航空交通控制系统时不应限制飞机的数目,不希望由于实
际飞机的数目超过设计值而导致系统终止。在 C A D 系统设计中,可以安排许多造型,但是只
有用户能确定到底需要多少造型。我们一旦注意到上述问题,便可在程序开发中发现许多这
样的例子。
依赖虚存储器去处理“存储器管理”的 C程序员常常发现 n e w、d e l e t e和包容器类思想的
混乱。表面上看,创建一个囊括任何可能需求的 h u g e 型全局数组是可行的,这不必有很多考
虑(并不需要弄清楚 m a l l o c ( )和f r e e ( ) ),但是这样的程序移植性较差,而且暗藏着难以捕捉的
错误。
另外,创建一个h u g e型全局数组对象,构造函数和析构函数的开销会使系统效率显著地下
降。C + + 中有更好的解决方法:将所需要的对象用 n e w 创建并将其指针放入包容器中,待实际
使用时将其取出。该方法确定了只有在绝对需要时才真正创建对象。所以在启动系统时可以忽
略初始化条件,在环境相关的事件发生时才真正创建对象。
在大多数情况下,我们应当创建存放感兴趣的对象的包容器,应当用 n e w 创建对象,然后
把结果指针放在包容器中(在这个过程中向上映射),具体使用时再将指针从包容器中取出。
该技术有很强的灵活性且易于分类组织。
15.2 模板综述
现在出现了一个问题, i s t a c k 可存放整数,但是也应该允许存放造型、航班、工厂等等数
据对象,如果这种改变每次都依赖源码的更新,则不是一个明智的办法。应该有更好的重用
第15章 模板和包容器类 297
下载
方法。
有三种源代码重用方法:用于契约的 C方法;影响过 C + + 的S m a l l t a l k方法;C + +的模板方
法。
15.2.1 C方法
毫无疑问,应该摒弃C方法,这是由于它表现繁琐、易发生错误、缺乏美感。如果需要拷
贝s t a c k的源码并对其手工修改,还会带入新的错误。这是非常低效的技术。
15.2.2 Smalltalk 方法
S m a l l t a l k方法是通过继承来实现代码重用的,既简单又直观。每个包容器类包含基本通用
类o b j e c t 的所属项目。S m a l l t a l k的基类库十分重要,它是创建类的基础。创建一个新类必须从
已有类中继承。可以从类库中选择功能和需求接近的已有类作为父类,并在对父类的继承中加
以修正从而创建一个新类。很明显这种方法可以减少我们的工作而提高效率(因此花大量的时
间去学习S m a l l t a l k类库是成为熟练的S m a l l t a l k 程序员的必由之路)。
所以这意味着S m a l l t a l k的所有类都是单个继承树的一部份。当创建新类时必须继承树的某
一枝。大多数树是已经存在的(它是 S m a l l t a l k 的类库),树的根称作o b j e c t——每个S m a l l t a l k包
容器所包含的相同的类。
这种方法表现出的整洁明了在于 S m a l l t a l k类层次上的任何类都源于 o b j e c t的派生,所以任
何包容器可容纳任何类,包括包容器本身。基
于基本通用类的(常称为 o b j e c t )的单树形层
次模式称为“基于对象的继承”。我们可能听
说过这个概念,并猜想这是另一个 O O P的基本
概念,就像“多态性”一样。但实际上这仅仅
意味着以o b j e c t (或相近的名称)为根的树形
类结构和包含o b j e c t的包容器类。
由于S m a l l t a l k类库的发展史较C + + 更长久,
且早期的 C + + 编译器没有包容器类库,所以
C + + 能将S m a l l t a l k 类库的良好思想加以借鉴。
这种借鉴出现在早期的C + + 实现中
[1]
,由于它表现为一个有效的代码实体,许多人开始使用它,
但把它用于包容器类的编程时则发现了一个问题。
该问题在于,在 S m a l l t a l k 中,我们可以强迫人们从单个层次结构中派生任何东西,但在
C + +中则不行。我们本来可能拥有完善的基于 o b j e c t的层次结构以及它的包容器类,但是当我
们从其他不用这种层次结构的供应商那里购买到一组类时,如造型类、航班类等等(层次结构
增加开销,而C程序员可以避免这种情况),我们如何把这些类树集成进基于 o b j e c t 的层次结构
的包容器中呢?这些问题如下所示:
由于C + + 支持多个无关联层次结构,所以 S m a l l t a l k 的“基于o b j e c t 的层次结构”不能很好
地工作。
解决方案似乎是明显的。如果我们有许多继承层次结构,我们就应当能从多个类继承:多
重继承可以解决上述问题。所以我们应按下述的方法去实施:
298 C + +编程思想
下载
[1] OOPS库,Keith Gorlen在N I H时开发的。一般以公开源代码的形式使用。
图 15-1
不从Object派生
存放指向对
象的指针
o s h a p e具有s h a p e的特点和行为,但它也是o b j e c t的派生类,所以可将其置于包容器内。
但是原先的C + + 并不包含多重继承,当包容
器问题出现时,C + +供应商被迫去增加多重继承
的特性。另外一些程序员一直认为多重继承不
是一个好主意,因为它增加了不必要的复杂性。
那时一句再三重复的话是“C + +不是S m a l l t a l k”,
这意味着“不要把基于 o b j e c t的层次结构用于包
容器类”。但最终
[ 1 ]
,由于不断的压力,还是把
多重继承加入到该语言中了。编译器供应商将
基于o b j e c t 的包容器类层次结构加入产品中并进
行了调整,它们中的大多数由模板来替代。我
们可以为多重继承是否可以解决大多数编程问题而进行争论,但是,在下一章中可以看到,由
于其复杂性,除某些特殊情况,最好避免使用它。
15.2.3 模板方法
尽管具有多重继承的基于对象的层次结构在概念上是直观的,但是在实践上较为困难。在
S t r o u s t r u p的最初著作
[2]
中阐述了基于对象层次的一种更可取的选择。包容器类被创造作为参
数化类型的大型预处理宏,而不是带自变量的模板,这些自变量能为我们所希望的类型替代。
当我们打算创建一个包容器存放某个特别类型时,应当使用一对宏调用。
不幸的是,上述方法在当时的S m a l l t a l k文献中被弄混淆了,加之难以处理,基本上没有什
么人将其澄清。
在此期间,S t r o u s t r u p 和贝尔实验室的 C + +
小组对原先的宏方法进行了修正,对其进行了
简化并将它从预处理范围移入了编译器。这种
新的代码替换装置被称为模板
[3]
,而且它表现
了完全不同的代码重用方法:模板对源代码进
行重用,而不是通过继承和组合重用对象代码。
包容器不再存放称为 o b j e c t 的通用基类,而由一个非特化的参数来代替。当用户使用模板时,
参数由编译器来替换,这非常像原来的宏方法,却更清晰、更容易使用。
现在,使用包容器类时关于继承和组合的忧虑可以消除了,我们可以采用包容器的模板版
本并且复制出和我们的问题相关的特定版本,像这样:
编译器会为我们做这些工作,而我们最终是以所需要的包容器去做我们的工作,而不是用
那些令人头疼的继承层次。在 C + + 中,模板实现了参数化类型的概念。模板方法的另一好处是
对继承不熟悉、不适应的程序新手能正确地使用密封的包容器类。
15.3 模板的语法
“模板(t e m p l a t e)”这一关键字会告诉编译器下面的类定义将操作一个或更多的非特定类
型。当对象被定义时,这些类型必须被指定以使编译器能够替代它们。
第15章 模板和包容器类 299
下载
图 15-2
[1] 我们也许决不能知道其全部,因为该语言的控制仍在 AT & T中。
[2] The C++Programming Language,由Bjarne Stroustrup著(第一版,A d d i s i o n - We s l e y, 1986)。
[3] 模板的灵感最初出现在 A D A。
图 15-3