STL简介(Introduction to the Standard Template Library)

昨天晚上花了一个晚上,翻译了一篇关于STL的文章。呵呵,第一次翻译这种东西,感觉计算机书籍还是英文原版的比较好,因为很多概念没法用中文恰当的表示(简直就是只可意会不可言传啊,:-))。由于第一次翻译,呵呵,水平肯定比较菜了,关键是对STL以前没看过,比较陌生,所以翻译得很辛苦。还是看英文原版的好,翻译的太辛苦了。

STL简介(Introduction to the Standard Template Library)

gshine 译

STL(标准模板库,Standard Template Library),是一个包含容器(container)类,算法(algorithm)和迭代器(iterators)的C++类库。它提供了许多计算机科学方面的基本算法和数据结构。STL是一个泛型(generic)库,这意味着它的各个组件(components)都已经最大程度的参数化了,基本上STL里面的所有组件都是一个模板(template)。所以,在你使用STL之前,必须保证你已经理解了C++中模板(template)是如何工作的。

容器和算法(container and algorithm)

和大多数类库一样,STL也包含容器类—它的主要目的是容纳其他的对象(通常是多个)。这些容器类包括以下几个类(classes):vector,list,deque,set,multiset,map,multimap,hash_set,hash_multiset,hash_map和hash_multimap。所有这些类都是模板类,即可以用来实例化之后容纳任何对象。比如,你可以像使用C语言里的数组一样使用vector<int>,但是vector<int>却为你省去了手动动态分配内存的麻烦。示例如下:

vector<int> v(3); // 声明一个包含3个元素的vector对象。

v[0] = 7;

v[1] = v[0] + 3;

v[2] = v[0] + v[1]; // v[0] = 7, v[1] =10, v[2] =17

STL同时也包含有大量用来操作存储在容器(container)里数据的算法集合。例如,你可以利用reverse算法来对一个vector里面的元素进行逆序操作。

reverse(v.begin(), v.end()); // v[0] =17, v[1] =10, v[2] =7

对reverse的调用,有两个需要注意的关键点。第一,这个函数本事是一个全局函数,并不是一个类中的成员函数。第二,它有两个参数,而非一个:也就是说,它是对一个容器中某个范围里的元素进行操作,而不是针对整个容器。当然,在本例中,被操作的元素范围正好是整个容器v。

造成上述2个情况的主要原因是:reverse,和其他的STL算法一样,它们是和容器类相分离的。这意味着,reverse函数,不仅仅可以用来逆序vector中的元素,也可以逆序list中的元素,甚至也可以用来逆序普通C语言数组中的元素。所以,下面的程序也是合法的:

double A[6] = { 1.2, 1.3, 1.4, 1.5, 1.6, 1.7 };

reverse(A, A + 6);

for (int i = 0; i < 6; ++i)

cout << "A[" << i << "] = " << A[i];

就像逆序一个vector一样,这个例子也是用了一个范围:reverse的第一个参数指向这个范围的起始点的指针,第二个参数是指向这个范围的最后一个元素的下一个元素。这个范围可以表示为[A,A+6);注意,前面是用"[",而结束用的是")",这意味着,A是范围的起点,但是A+6不是范围的最后一个点,而是结束点的下一个元素(")"表示不包含)。

迭代器(Iterators)

在上面那个逆序C语言数组的例子中,传给reverse函数的参数很明显是一个double*。那么,当你用reverse函数来逆序一个vector或者list对象时,你传给这个函数的参数应该是什么类型呢?也就是说,reverse函数声明时,它的形式参数到底是什么类型呢,而v.begin()和v.end()返回的又是什么类型呢?

答案是reverse函数的形参类型是迭代器类型,它其实是一个泛化的指针。指针本身就是一种迭代器,这就是reverse为什么可以逆序一个数组的原因。与此类似,vector类里面嵌套声明了iterator和const_iterator类型。在上面的例子中,v.begin()和v.end()返回值的类型就是vector<int>::iterator。除此之外,还有一些其他的迭代器(ierators),比如istream_iterator和ostream_iterator,但是它们和容器没有任何关系。

迭代器是实现算法和容器分离的主要机制:算法是用模板实现的,并且用迭代器参数化,因此,它们不必被限制在一种容器之中。比如,试想如何写一个算法来实现在一个线性表某个范围内查找元素。下面是STL实现用的算法find():

template <class InputIterator, class T>

InputIterator find(InputIterator first, InputIterator last, const T& value) {

while (first != last && *first != value) ++first;

return first;

}

Find函数有三个参数:两个迭代器用于定义查找范围,另一个是要在该范围中查找的值。该函数从头到尾检查其中的每个迭代器,直到它找到参数value指定的要查找的值或者到达了范围的末尾才停止它的查找。

First 和last 被定义为InputIterator类型,它是一个模板参数。也就是说,实际上不存在叫做“InputIterator”的类型:当你调用find函数时,编译器把你传来的实参的类型替换掉原来的类型参数InputIterator和T。所以,如果你传递的实参中,前两个参数是int*,第三个是int,那么,你实际上是在调用下面的函数:

int* find(int* first, int* last, const int& value) {

while (first != last && *first != value) ++first;

return first;

}

概念和模型(Concepts and Modeling)

[译者注:感觉Concept就像接口,定义功能需求;而Model就像类,实现接口的功能。因此,下文中Concept和Model基本没怎么翻译,直接引用原单词]

关于模板函数,当然不仅仅是STL的算法,一个很重要的问题就是什么样的类型能够准确的替换形式模板参数。例如,很明显,int*和double*能够替换find函数的形式模板参数InputIterator。当然,int和double不能替换InputIterator,这也是很显然的。Find函数中使用了表达式*first,但是,对int和double的对象来说,执行(*)操作是没有意义的。比如,int x;然后对x执行*x是不合适的,也是不允许的。所以,基本的答案是,find函数隐式的定义了一系统对参数类型的要求,只有满足这些要求的类型才能够使用它。比如,要想能够替换InputIterator的类型必须提供某些特定操作:该类型的两个对象之间必须能够比较是否相等,必须能够实现实现自增操作,还有必须能够实现(*)操作来得到它指向的对象等等。

Find函数不是唯一的有上述要求的STL算法:for_each和count等其他算法也同样需要这些要求。由于这些要求非常重要,因此我们给这些一系列的要求命名concept,比如我们称满足上面所述的要求的concept为InputIterator。一个类型如果满足了那些要求,我们就说它遵循一个concept,或者说它是一个concept的例子(model)。我们称int*是一个InputIterator的model,是因为int*提供了InputIterator这个concept的所有的要求实现的操作。

Concept不是C++语言的一部分,我们无法在程序中声明一个concept,或者声明一个特定的类型是某个concept的model。不过,concept是STL中很重要的一部分。使用concept很容易实现在程序中将接口和实现相分离。Find函数的作者仅仅需要考虑InputIterator这个concept的接口,而不需要考虑遵循InputIterator这个concept的所有类型。同样的,如果你想使用find这个函数,你也仅仅需要保证你传给它的实参是InputIterator的一个model。这就是find和reverse函数能够接受lists,vectors,数组等其他类型作为参数的原因。所以,利用concept,而不是特定的类型编程,能够实现软件组件的重用性和易于集成。

细化(Refinement)

实际上,InputIterator是一个很弱的concept,因为它仅仅施加了很少的要求。一个InputIterator必须支持指针运算的一个子集(必须实现自增运算,无论是前置还是后置),但是没有要求实现指针运算的所有操作。这对find函数来说已经足够,但是对于其他的算法,可能需要它们的参数实现更多的要求。比如,reverse函数,必须能够自增和自减它的参数:它使用了表达式--last。借助于concept,我们说reverse的参数必须是BidirectionalIterrator而非InputIterator的一个model。

BidirectionalIterrator这个concept很InputIterator很相像:它仅仅增加了一些额外的要求。BidirectionalIterrator的models 是InputIterator的models 的一个子集。如果一个类型它是BidirectionalIterrator的一个model,则它也是InputIterator的一个model。比如,int*既是BidirectionalIterrator的一个model,也是InputIterator的一个model。但是istream_iterator仅仅是InputIterator的一个model,它不符合要求等严厉的BidirectionalIterrator的要求。

对于InputIterator和BidirectionalIterrator的关系,我们称之为BidirectionalIterrator是InputIterator的一个细化(refinement)。Concept之间的细化(refinement)就像C++类之间的继承关系。我们之所以使用不同的称法,是为了强调细化应用于concept而继承应用于实际的类型。

除了我们介绍的两个外,实际上还有3个迭代器concept。这五个iterator concept是 Output Iterator, Input Iterator, Forward Iterator, Bidirectional Iterator, and Random Access Iterator;ForwardIterator是InputIterator的细化,BidirectionalIterrator是ForwardIterator的细化,RandomAccessIterator是BidirectionalIterrator的细化。(OutputIterator和其它四个concept有关系,但是它不在这个细化表中,它不是其它iterator concept的细化,并且其它的iterator concept也不细化它。)

容器类,就像迭代器一样,也被组织成concept的层次表。所有的容器都是Container这个concept的model。更加细化的concept,比如像Sequence和AssociateContainer,用于描述特定类型的容器。

STL的其它部分

如果你弄懂了算法,迭代器和容器,那么你基本上弄懂了STL的所有东西。不过,STL还包括一些其他类型的组件(components)。

首先,STL还包括一些基本的实用的工具(utilities):这些基本的concept和函数会在整个类库的不同部分被用到。比如,Assignable这个concept,它要求类型必须实现赋值操作和复制构造函数。基本上所有的STL类都是Assignable的models,并且基本上所有的STL算法都要求它们的参数是Assignable的models。

其次,STL包含一些分配和释放内存的机制。分配运算符(Allocators)是很专业的,你通常可以很安全的忽略他们,无论是出于什么目的。

最后,STL包含一个很大的关于函数对象(function object)的集合,也叫做functors。就像迭代器是指针的泛化,函数对象是函数的泛化:你可以像调用普通函数那样使用一个函数对象。有许多种不同的和函数对象相关联的concept,包括Unary Function(可以接受一个参数的函数对象)和Binary Function(可以接受2个参数的函数对象)。函数对象是泛型编程的一个很重要的部分,因为它使抽象不仅发生在对象类型上,而且可以应用于操作这些对象的函数上。

原文出处:http://www.sgi.com/tech/stl/stl_introduction.html

gshine

2008-10-19晚

于dlut

posted @ 2008-10-20 09:09  gshine  阅读(931)  评论(0编辑  收藏  举报