现在的位置: 首页 > 综合 > 正文

标准模板库STL简介

2013年12月10日 ⁄ 综合 ⁄ 共 4616字 ⁄ 字号 评论关闭

题   目:Introduction
to the Standard Template Library

原作者:SGI
翻   译:btwsmile

标准模板库(STL)是一个有关容器类、算法和迭代器的C++程序组件库,它提供了计算机科学领域内基础的算法和数据结构。STL是一个泛型库,这意味着它的组件被广泛的参数化,几乎每一个组件都用模板来实现。在使用STL之前,你必须首先理解C++模板是如何工作的。

容器和算法

和许多类库一样,STL也包含容器类。所谓容器类,顾名思义,就是用于容纳其它对象的类。STL提供的容器类主要有:vector,list,deque,set,multiset,map,multimap,hash_set,hash_multiset,hash_map以及hash_multimap,这些类都是模板类,都能够被实例化,以容纳任意一种类型的对象。比如,你可以像使用普通C数组一样地使用vector<int>,而vector帮你实现了动态内存的管理,而这原本是很麻烦的地方。阅读下面的代码:

vector<int> v(3);       // 声明一个含有3个元素的向量
v[0] = 7;
v[1] = v[0] + 3;
v[2] = v[0] + v[1];     // 3个元素依次为7,10,17

STL也包括一系列的算法,它们可以对容器中所保存的数据进行处理。比如,你可以使用reverse算法将vector中的所有元素进行翻转:

reverse(v.begin(), v.end());     // 翻转后3个元素依次为17,10,7

调用reverse函数时,需注意以下两点:1)它是全局函数,而非任何类的成员函数。2)它需要两个参数,而不是一个。也就是说,reverse作用于一个元素区间,而非某一个容器。只不过在上面的例子中,reverse所翻转的元素区间刚好包括向量v的所有元素,这只是一个巧合。
之所以要这样设计,是因为在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];

与翻转向量v一样,上面的例子使用了一个区间:第一个参数表示区间的头指针,而第二个参数指向该区间最后一个元素的后一个位置。这个区间可被表示成[A, A+6),之所以是半开区间,是因为两个端点是不同的。A表示该范围的第一个有效位置,而A+6表示的是该区间最后一个有效位置的下一个位置,A+6本身是无效的位置。

迭代器

在翻转普通C数组的例子中,被翻转的参数显然是double*类型,但是当你使用vector或者list的时候,传入reverse函数的参数是什么类型呢?换句话说,reverse函数所声明的参数类型到底是什么?v.begin()和v.end()的返回值是什么类型?
答案就是:reverse函数所使用的参数类型是迭代器,它是指针的一种泛化。指针本身就是迭代器,这也解释了为什么reverse函数可以翻转普通C数组。类似的,vector内嵌声明了两种数据类型iterator和const_iterator。在上面的例子中,v.begin()和v.end()返回值的类型就是vector<int>::iterator。并非所有迭代器都是内嵌定义的,比如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函数含有3个参数,其中两个是迭代器,用于表示区间,另外一个表示在该区间内所查找的值。它按照从前往后的顺序依次检查[first, last)内的每一个迭代器,直到找到指向value的迭代器或者到达区间末尾才停止。
first和last被声明为InputIterator类型,而InputIterator是模板参数,也就是说并没有一种叫InputIterator的类型,当你调用find函数时,编译器将使用真正的参数类型来替换模板类型参数InputItrator和T。假设find的前两个参数类型为int*,第三个参数类型为int,那么当你调用find函数时,就相当于调用了下面的函数:

int* find(int* first, int* last, const int& value) {
     while(first != last && *first != value)
          ++first;
     return first;
}

concept和model

不仅对STL算法,对任何一个模板函数都应该考虑下面这个问题,即该模板函数所允许的可以正确替换模板参数的类型集合是什么。比如说,int*或者double*显然可以正确替换find函数中模板参数InputIterator,而int或者double显然不能正确替换InputIterator。因为find函数内部使用了*first这样的表达式,对一个int或double类型的对象来说,对它实行解引用操作是没有任何意义的。因此可以得到一个基本的结论:find函数隐含了对参数类型InputIterator的一组要求,只要满足这些要求的任何类型都可以用来实例化函数模板find。无论是哪种类型,只要它想要成功替换InputIterator,就必须能比较该类型的两个元素是否相等,也要能对该类型进行自加操作,还要能对该类型的对象实行解引用操作从而获得它所指向的对象等。
find函数并非STL中唯一有此要求的算法,比如for_each、count等其它函数也必须满足同样的要求。这类要求非常重要,所以我们给它们一组专门的名称。具体来说,我们称这样的一组类型需求为concept,而将find等函数所对应的特定concept称为“输入迭代器”。当一种类型满足了某种concept的所有类型需求,我们就称这种类型是该concept的一个model。基于这样的定义,int*就是“输入迭代器”的model,因为int*满足了“输入迭代器”所指定的所有操作。
concept并不是C++的一部分,所以并没有一种在程序中声明一种concept的方法,当然也没有一种方法可以声明某种类型是某concept的model。然而,concept却是STL极其重要的部分。使用concept可帮助我们编写出接口与实现相互分离的程序:find函数的作者只需考虑“输入迭代器”所指定的接口,而不必担心每一种可能满足“输入迭代器”的数据类型的所有实现。类似地,如果你想要使用find函数,你唯一需要做的就是考虑传递给它的参数是否为“输入迭代器”的model。这就是为什么find、reverse函数能同时作用于list、vector、普通C数组以及其它类型的原因。基于concept而非基于具体的数据类型来进行程序设计,使得复用和组装程序组件成为可能。

refinement

“输入迭代器”实际上是一个相当弱的concept,换句话说,它所附加的要求比较少。一个“输入迭代器”必须支持指针运算的一个子集(它必须能够对一个“输入迭代器”进行opeartor++操作),但并没必要支持所有的指针运算。对于find函数来说,“输入迭代器”已经足够,但是对于其它一些算法而言,其参数可能需满足额外的要求。比如reverse算法还要求它的参数能够进行opeator--操作,因为reverse内部使用了--last这样的表达式。用concept术语来说,reverse的参数必须是“双向迭代器”的model。
“双向迭代器”concept与“输入迭代器”类似,只不过它附加了额外的要求。所有“双向迭代器”的model同时也是“输入迭代器”model的子集,也就是说“双向迭代器”中的每一个model同时也是“输入迭代器”的model。比如int*既是“双向迭代器”又是“输入迭代器”的model,但是istream_iterator却只是“输入迭代器”的model,因为istream_iterator并不满足“双向迭代器”更高的要求。
在描述“双向迭代器”和“输入迭代器”之间的关系时,我们可以说“双向迭代器”是“输入迭代器”的refinement。refinement这个概念与C++类的“继承”概念很类似,而我们之所以使用不同的词语来表示,是为了强调refinement应用于concept而非实际的数据类型。
实际上,除了前述“输入迭代器”和“双向迭代器”,STL中还有另外3种迭代器,依次为“输出迭代器”、“前向迭代器”以及“随机访问迭代器”。它们之中,“前向迭代器”是“输入迭代器”的refinement,“双向迭代器”则是“前向迭代器”的refinement,而“随机访问迭代器”又是“双向迭代器”的refinement。需要特别注意的是,“输出迭代器”虽然与另外四种迭代器有所关联,但它并不是迭代器refinement体系中的一员,也就是说它不是任何迭代器的refinement,也没有哪一种迭代器是它的refinement。更多有关迭代器的信息,请查看《迭代器概览》。
与迭代器类似,容器类也被组织成为concept层次体系结构。所有的容器类都是“容器”这一concept的model,而更具体的concept,比如sequence和associative容器,描述的则是更为具体的容器类型。

STL的其它部分

如果你理解算法、迭代器以及容器,那么你基本上已经完全明白STL了。不过,STL也包括了其它一些部分。
1)在STL模板库的不同地方使用了一些相当基础的concept和函数。打个比方,assignable是描述数据类型能支持赋值操作符和拷贝构造方法的concept,几乎所有的STL容器类都是assignable的model,同时几乎所有的STL算法都要求它们的参数是assinable的model。
2)一些用于分配和释放内存的底层机制。内存分配器的功能是很强大的,几乎在任何时候使用,你都无需担心它的安全问题。
3)很多的函数对象,或称算子。如果说迭代器是指针的泛化,那么函数对象就是函数的泛化。你可以像使用普通函数那样调用函数对象。与函数对象相关联的concept主要有两个,“单目函数”和“双目函数”。前者是单参数的函数,其调用语法为f(x);后者是双参数的函数,其调用语法为f(x, y)。函数对象是泛型编程重要的部分,因为它不但抽象了对象的类型,还抽象了所执行的操作。

翻译修订历史:
2012-09-10 首次发布。

抱歉!评论已关闭.