SGI

标准模板库简介

标准模板库,或称STL,是一个包含容器类、算法和迭代器的C++库;它提供了许多计算机科学的基本算法和数据结构。STL是一个泛型库,这意味着它的组件被大量参数化:STL中的几乎每个组件都是一个模板。在使用STL之前,您应该确保理解C++中模板的工作原理。

容器和算法

与许多类库一样,STL包含容器类:其目的是包含其他对象的类。STL包含以下类:vector, list, deque, set, multiset, map, multimap, hash_set, hash_multiset, hash_map以及hash_multimap。每个类都是一个模板,可以实例化以包含任何类型的对象。例如,您可以使用vector<int>,其方式与使用普通的C数组非常相似,只是vector消除了手动管理动态内存分配的麻烦。

      vector<int> v(3);            // Declare a vector of 3 elements.
      v[0] = 7;
      v[1] = v[0] + 3;
      v[2] = v[0] + v[1];          // v[0] == 7, v[1] == 10, v[2] == 17  

STL还包含大量用于操作存储在容器中的数据的算法。例如,您可以使用vector,例如,通过使用reverse算法来反转元素的顺序。

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

关于此对reverse的调用,需要注意两点。首先,它是一个全局函数,而不是成员函数。其次,它接受两个参数而不是一个:它操作的是一个元素的范围,而不是一个容器。在本例中,该范围恰好是整个容器v。

这两个事实的原因相同reverse,像其他STL算法一样,与STL容器类解耦。这意味着reverse不仅可以用于反转向量中的元素,还可以用于反转列表中的元素,甚至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);非对称的表示法提醒我们两个端点是不同的,第一个是范围的开头,第二个是范围末尾之后的元素。

迭代器

在反转C数组的示例中,对reverse的参数显然是double*类型。但是,如果您要反转vectorlist,那么reverse的参数是什么?也就是说,reverse究竟将参数声明为什么类型,以及v.begin()v.end()返回什么?

答案是对reverse的参数是迭代器,它是指针的泛化。指针本身就是迭代器,这就是可以反转C数组元素的原因。类似地,vector声明嵌套类型iteratorconst_iterator。在上面的示例中,v.begin()v.end()返回的类型是vector<int>::iterator。还有一些迭代器,例如istream_iteratorostream_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;
      }

查找接受三个参数:两个定义范围的迭代器,以及要在该范围内搜索的值。它检查范围中的每个迭代器[first, last),从头到尾进行,并在找到指向value的迭代器或到达范围末尾时停止。

Firstlast被声明为InputIterator以及InputIterator类型是一个模板参数。也就是说,实际上没有名为InputIterator的类型:当您调用find时,编译器会将参数的实际类型替换为形式类型参数InputIteratorT。如果对find的前两个参数是int*类型,第三个参数是int类型,那么就像您调用了以下函数一样。

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

概念和建模

关于任何模板函数(不仅仅是STL算法)要问的一个非常重要的问题是,可以正确地替换形式模板参数的类型集是什么。例如,很明显,int*double*可以替换为find的形式模板参数InputIterator。同样明显的是,intdouble不可以find使用表达式*first,而解引用运算符对于intdouble类型的对象没有意义。因此,基本的答案是find隐式地定义了一组对类型的要求,并且可以使用满足这些要求的任何类型对其进行实例化。替换为InputIterator的任何类型都必须提供某些操作:必须能够比较该类型的两个对象是否相等,必须能够递增该类型的对象,必须能够解引用该类型的对象以获取它指向的对象,等等。

查找不是唯一具有此类要求集的STL算法;对for_eachcount和其他算法的参数必须满足相同的要求。这些要求非常重要,因此我们为它们命名:我们称这样的类型要求集为概念,并将此特定概念称为输入迭代器。我们说一个类型符合一个概念,或者说是一个概念的模型,如果它满足所有这些要求。我们说int*输入迭代器的模型,因为int*提供了输入迭代器要求中指定的所有操作。

概念不是C++语言的一部分;无法在程序中声明概念,也无法声明特定类型是某个概念的模型。然而,概念是STL中极其重要的部分。使用概念可以编写将接口与实现清晰分开的程序:find的作者只需要考虑输入迭代器概念指定的接口,而不是考虑符合该概念的每种可能类型的实现。类似地,如果您想使用find,您只需要确保传递给它的参数是输入迭代器的模型。这就是为什么findreverse可以与lists、vectors、C数组和许多其他类型一起使用的原因:根据概念而不是根据特定类型进行编程,可以重用软件组件并将组件组合在一起。

细化

输入迭代器实际上是一个相当弱的概念:也就是说,它施加的要求很少。输入迭代器必须支持指针算术的一个子集(必须能够使用前缀和后缀operator++递增输入迭代器),但不需要支持指针算术的所有操作。这对于find来说已经足够了,但其他一些算法要求其参数满足其他要求。反转,例如,必须能够递减其参数以及递增它们;它使用表达式--last。从概念的角度来看,我们说reverse的参数必须是双向迭代器而不是输入迭代器的模型。

双向迭代器概念与输入迭代器概念非常相似:它只是施加了一些额外的要求。双向迭代器的模型类型是输入迭代器的模型类型的子集:每个双向迭代器的模型类型也是输入迭代器的模型类型。Int*,例如,既是双向迭代器的模型,也是输入迭代器的模型,但istream_iterator,仅是输入迭代器的模型:它不符合更严格的双向迭代器要求。

我们通过说双向迭代器输入迭代器细化来描述输入迭代器双向迭代器之间的关系。概念的细化非常类似于C++类的继承;我们使用不同的词而不是简单地将其称为“继承”的主要原因是强调细化适用于概念而不是实际类型。

除了我们已经讨论过的两个迭代器概念之外,实际上还有三个迭代器概念:五个迭代器概念是输出迭代器输入迭代器前向迭代器双向迭代器随机访问迭代器前向迭代器输入迭代器的细化,双向迭代器前向迭代器的细化,随机访问迭代器双向迭代器的细化。(输出迭代器与其他四个概念相关,但它不是细化层次结构的一部分:它不是任何其他迭代器概念的细化,任何其他迭代器概念也不是它的细化。)迭代器概述提供了有关迭代器的一般信息。

容器类,像迭代器一样,也组织成一个概念层次结构。所有容器都是容器概念的模型;更精细的概念,如序列关联容器,描述特定类型的容器。

STL的其他部分

如果您理解了算法、迭代器和容器,那么您就几乎了解了关于STL的所有内容。但是,STL确实包含其他几种类型的组件。

首先,STL包含几个实用程序:库中许多不同部分中使用的非常基本的概念和函数。例如,可赋值概念描述了具有赋值运算符和复制构造函数的类型;几乎所有STL类都是可赋值的模型,几乎所有STL算法都要求其参数是可赋值的模型。

其次,STL包含一些用于分配和释放内存的底层机制。分配器非常专业,在几乎所有情况下都可以安全地忽略它们。

最后,STL包含大量函数对象,也称为仿函数。正如迭代器是指针的泛化一样,函数对象是函数的泛化:函数对象是可以使用普通函数调用语法调用的任何内容。有几个与函数对象相关的不同概念,包括一元函数(一个接受单个参数的函数对象,一个被调用为f(x)) 和二元函数(一个接受两个参数的函数对象,一个被调用为f(x, y))。函数对象是泛型编程的重要组成部分,因为它们不仅允许对对象类型进行抽象,还允许对正在执行的操作进行抽象。


[Silicon Surf] [STL Home]
版权所有 © 1999 Silicon Graphics, Inc. 保留所有权利。 商标信息