C++语言提供的标准模板库(Standard Template Library,STL)是面向对象程序设计与泛型程序设计思想相结合的一个良好典范。
泛型程序设计:就是编写不依赖于具体数据类型的程序。主要思想是将算法从特定的数据结构中抽象出来,使算法成为通用的、可以作用于各种不同的数据结构。这样就不必为每种容器都编写一套同样的算法,当容器模板修改、扩充时也不必重写大量的算法函数,从而提高软件的复用性。
C++中,模板是泛型程序设计的主要工具。
可以用概念来描述泛型程序设计中作为参数的数据类型所需具备的功能。
概念(concept):泛型程序设计中的一个术语,它的内涵是这些功能,它的外延是具备这些功能的所有数据类型。
例如,“可以比大小、具有公有的复制构造函数并可以用‘=’赋值的所有数据类型”就是一个概念,可以将这个概念记作Sortable。
模型(model):具备一个概念所需要功能的数据类型成为这一概念的一个模型。
例如,int数据类型就是Sortable概念的一个模型。
概念之间有包含和被包含的关系:对于两个不同的概念A和B,如果概念A所需求的所有功能也是概念B所需求的功能(即概念B的模型一定是概念A的模型),那么就说概念B是概念A的子概念(有些书上又把它称为精炼(refinement))。
例如,如果把“可以比大小的所有数据类型”这一概念记作Comparable,把“具有公有的复制构造函数并可以用‘=’赋值的数据类型”这一概念记作Assignable,那么Sortable既是Comparable的子概念,也是Assignable的子概念。
提示:在下面各节中,将为每一个概念赋予一个名称,并使用该名称作为模板参数名,例如,将用下面的方式表示insertionSort
这样一个函数模板的原型:
template //insertionSort函数模板原型
void insertionSort(Sortable a[], int n);
STL即标准模板库最初是由HP公司的Alexander Stepanov和Meng Lee开发的一个用于支持C++泛函编程的模板库,1994年被纳入C++标准,成为C++标准库的一部分。由于C++标准库有多种不同的实现,因此STL也有不同的版本,但它们为用户提供的接口都遵循相同的标准。
STL提供了一些常用的数据结构和算法,例如vector就是STL提供的一个容器(以后将它称为向量容器),链表在STL中也有对应容器,排序、顺序查找、折半查找等算法在STL中都有现成的函数模板。
STL的更大意义在于,它定义了一套概念体系,为泛型程序设计提供了逻辑基础。STL中的各个类模板、函数模板的参数都是用这个体系中的概念来规定的。使用STL的一个模板时所提供的类型参数既可以是C++标准库中已有的类型,也可以是自定义的类型——只要这些类型是所要求概念的模型,因此,STL是一个开放的体系。
STL所涉及的四种基本组件:容器、迭代器、函数对象、算法。
下面是一个最简单的STL程序 :
#include
#include
#include
#include
#include
using namespace std;
int main() {
const int N = 5;
vectors(N); //定义一个大小为N的向量容器
//从标准输入读入向量容器的内容
for (int i = 0; i != N; i++)
cin >> s[i];
//输出向量容器中每个元素的相反数
transform(s.begin(), s.end(), ostream_iterator(cout, " "), negate());
cout << endl;
return 0;
}
程序执行后,输入4 -2 1 0 5
时,输出为-4 2 -1 0 -5
。
上例已经包括了STL所涉及的四种基本组件。先简单介绍如下。
容器(container)是容纳、包含一组元素的对象。
容器库类中包括七种基本容器:向量(vector)、双端队列(deque)、列表(list)、集合(set)、多重集合(multiset)、映射(map)、多重映射(multimap)。
这七种容器可以分为两种基本类型:顺序容器(sequence container)和关联容器(associative container)。
顺序容器将一组具有相同类型的元素以严格的线性形式组合起来,向量、双端队列和列表容器就属于这一种。
关联容器具有根据一组索引来快速提取元素的能力,集合和映射容器就属于这一种。
使用不同的容器,需要包含不同的头文件。
迭代器(iterator)提供顺序访问容器中每个元素的方法。
指针本身就是一种迭代器,迭代器是泛化的指针。
使用独立于STL容器的迭代器,需要包含头文件
。
上例中,其中,s.begin()
,s.end()
, ostream_iterator
都是迭代器。s.begin()
指向的是向量容器s的第一个元素, s.end()
指向的是向量容器s的末尾(最后一个元素的下一个位置)。ostream_iterator
是一个输出迭代器,其中,ostream_iterator
是一个输出迭代器的类模板,上例中通过执行它的构造函数来建立一个输出迭代器对象。ostream_iterator
的实例并不指向STL容器的元素,而是指向一个输出流。上例中,输出迭代器关联到的输出流cout
和分隔符" "
都是通过构造函数提供的。
函数对象(function object)是泛化的函数,是一个行为类似函数的对象,可以像调用函数一样调用函数对象。任何普通的函数和任何重载了“()
”运算符的类的对象都可以作为函数对象使用。
使用STL的函数对象,需要包含头文件
。
上例中,negate
就是一个函数对象。negate
是一个类模板,它重载了“()
”运算符,接收一个参数,该运算符返回的就是该参数的相反数。negate
的模板参数int
表示的是negate
的“()
”运算符接收和返回参数的类型。
STL包括七十多个算法,包括查找算法、排序算法、消除算法、计数算法、比较算法、变换算法、置换算法、容器管理等。这些算法的一个最重要的特性就是它们的统一性,并且可以广泛用于不同的对象和内置的数据类型。
使用STL的算法,需要包含头文件
。
上例中调用的transform
就是一个算法,为了说明该算法的用途,下面给出该算法的一种实现:
template
OutputIterator transform(InputIterator first, InputIterator last, OutputIterator result, UnaryFunction op){
for(; first!=last; ++first, ++result)
*result=op(*first);
return result;
}
该算法遍历first
和last
两个迭代器所指向的元素,将每个元素的值作为函数对象op
的参数,将op
的返回值通过迭代器result
顺序输出。遍历完成后result
迭代器所指向的是输出的最后一个元素的下一个位置,transform
会将该迭代器返回,上例中忽略了这个返回的值。
STL把迭代器作为算法的参数,通过迭代器来访问容器,而不是把容器直接作为算法的参数。
STL把函数对象作为算法的参数,而不是把函数所执行的运算作为算法的一部分。
STL的算法利用迭代器对存储在容器中的元素序列进行遍历(迭代器是算法和容器的“中间人”)。迭代器提供了访问容器中每个元素的方法。
指针本身就是一种迭代器,迭代器是泛化的指针。虽然指针是一种迭代器,但迭代器却并不仅仅是指针。指针可以指向内存中的一个地址,通过这个地址可以访问相应的内存单元;而迭代器更为抽象,它可以指向容器中的一个位置,不必关心这个位置对应的真正物理地址,只需要通过迭代器访问这个位置的元素。
指针是算法和数据结构的“中间人”:遍历链表需要使用指针,对数组元素进行排序时也需要通过指针访问数组元素(数组名本身就是一个指针),指针便充当了算法和数据结构的“中间人”。
迭代器是算法和容器的“中间人”:在STL中,容器是封装起来的类模板,其内部结构无从知晓,而只能通过容器接口来使用容器。但是STL中的算法是通用的函数模板,并不专门针对某一个容器类型。算法要适用于多种容器,而每一种容器中存放的元素又可以是任何类型,这时普通指针就无法充当“中间人”了,必须要使用更为抽象的指针——迭代器,来作为算法和容器的“中间人”。就像声明指针时要说明其指向的元素一样,STL的每一个容器类模板中,都定义了一组对应的迭代器类。使用迭代器,算法函数可以访问容器中指定位置的元素,而无须关心元素的具体类型。
输入流迭代器、输出流迭代器都是类模板。
输入流迭代器用于从一个输入流中连续地输入某种类型的数据;输出流迭代器用于向一个输出流中连续地输出某种类型的数据。
cin
是输入流的一个实例,cout
是输出流的一个实例(输入输出流将在第11章详细介绍)。
**输入流迭代器:是一个类模板,用于从一个输入流中连续地输入某种类型的数据。**比如:
templateistream_iterator; //istream_iterator省略了后面几个有默认值的模板参数
提示:由于STL设计得非常灵活,很多STL的模板都有三四个模板参数(例如istream_iterator
实际上有多达四个模板参数),但排在后面的模板参数一般都有默认的参数值,绝大部分程序中都会省略这些参数而使用它们的默认值。
(为了避免给初学者造成不必要的麻烦,上面的istream_iterator
只给出了一个没有默认值的模板参数,后面的模板参数直接省略。本章中遇到的类似情况将不再给出说明。)
上例中,T
是使用该迭代器从输入流中输入数据的类型。类型T
需要满足两个条件:
(1)有默认构造函数;
(2)对该类型的数据可以使用“>>
”从输入流输入。
一个输入流迭代器的实例需要由下面的构造函数来构造:
istream_iterator(istream& in); //构造函数,用于构造输入流迭代器的实例
在该构造函数中,需要提供用来输入数据的输入流(例如cin
)来作为参数。
输入流结束的判断:istream_iterator
类模板有一个默认构造函数,用该默认构造函数构造出的迭代器指向的就是输入流的结束位置,将一个输入流于这个迭代器进行比较就可以判断输入流是否结束。
**输出流迭代器:是一个类模板,用于向一个输出流中连续地输出某种类型的数据。**比如:
templateostream_iterator; //ostream_iterator省略了后面几个有默认值的模板参数
上例中,T
是向输出流中输出数据的类型。类型T
只需满足一个条件(不同于输入流迭代器中要求T
还需要有默认构造函数):对该类型的数据可以使用“<<
”向输出流输出。
一个输出流迭代器的实例可以用下面两个构造函数来构造:
ostream_iterator(ostream& out); //构造函数,用于构造输出流迭代器的实例
ostream_iterator(ostream& out, const char* delimiter);
其中,构造函数的参数out
表示将数据输出到的输出流。参数delimiter
是可选的,表示两个输出数据之间的分隔符。
例子:从标准输入读入几个实数,分别将它们的平方输出:
#include
#include
#include
using namespace std;
double square(double x){ //求平方的函数
return x * x;
}
int main() {
//从标准输入读入若干个实数,分别将它们的平方输出
transform(istream_iterator(cin), istream_iterator(), ostream_iterator(cout, "\t"), square);
cout << endl;
return 0;
}
当输入为:
0.5 1.1 0 -3 0.1
输出为:
0.25 1.21 0 9 0.01
注意:由于该程序会从标准输入流中读取数据直到输入流结束,运行该程序时,输入完数据后,在Windows下需要按Ctrl+Z和回车键,在Linux下需要按Ctrl+D键,表示标准输入结束。后面凡是通过一对istream_iterator
读入数据的程序皆是如此。
引入输入流迭代器和输出流迭代器的意义:虽然输入流迭代器和输出流迭代器并不能比输入流和输出流提供更强大的功能,但由于它们采用迭代器的接口,在这两种迭代器的帮助下,输入流和输出流可以直接参与STL的算法。
输入流迭代器和输出流迭代器可以被看作适配器,它们将输入流和输出流的接口变更为迭代器的接口。
适配器(adapter):指用于为已有对象提供新的接口的对象,适配器本身一般并不提供新的功能,只为了改变对象的接口而存在。
STL中迭代器根据功能可分为五类:输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器。
五类迭代器又分别对应于五个概念。五个概念之间的关系:
(1)前向迭代器是输入迭代器的子概念,也是输出迭代器的子概念(即前向迭代器肯定是输入迭代器,也肯定是输出迭代器);
(2)双向迭代器是前向迭代器的子概念;
(3)随机访问迭代器是双向迭代器的子概念。
下面阐述符合这些概念的迭代器所具备的功能:
先约定:
符号 | 含义 |
---|---|
P | 一种迭代器数据类型 |
p1,p2 | P类型迭代器的对象 |
T | P类型迭代器指向元素的数据类型 |
t | T类型的一个对象 |
m | 当T是类或结构体时,T中任意一个可访问到的成员 |
n | 一个整数 |
注意:本章所介绍的所有概念都是10.1节所述的Assignable概念的子概念,也就是说,这些概念都有公有的复制构造函数和赋值运算符,因此在具体介绍概念时不再重复这两个性质。
下面是除了都有公有的复制构造函数和赋值运算符之外,所有迭代器都具备的功能:
表达式 | 功能 | 返回值 |
---|---|---|
++p1 |
使迭代器指向下一个元素 | 该表达式的返回值为p1自身的引用 |
p1++ |
使迭代器指向下一个元素 | 该表达式的返回类型是“不确定”的(见下方提示) |
提示:上表中“返回类型是不确定的”中的“不确定”,并不是说对于每个迭代器实例,p1++的返回类型都是不确定的,而是说对于迭代器这一概念而言p1++的返回类型没有一致的定义。对前向迭代器来说,p1++的返回类型不再不确定,是因为p1++的返回类型的定义在前向迭代器这一外延更小的概念范围内是一致的。
输入迭代器可以从序列中读取数据,但不一定能够向序列中写入数据。
输入迭代器支持对序列进行不可重复的单向遍历。
输入流迭代器是一种典型的输入迭代器。
下面是在迭代器的通用功能之外,输入迭代器所具备的功能:
表达式 | 功能 | 返回值 |
---|---|---|
p1==p2 |
比较两个迭代器是否相同 | |
p1!=p2 |
比较两个迭代器是否不同,等价于!(p1==p2) |
|
*p1 |
获取输入迭代器所指向元素的值 | 返回值可以转换到T 类型(可以是T ,T& ,const T& 等类型) |
p1->m |
等价于(*p1).m |
|
*p1++ |
对输入迭代器而言,尽管p1++ 的返回类型是不确定的,但*p1++ 的返回值是确定的,是{T t=*p1; ++p1; return t;} |
注意:如果p1==p2
,并不能保证++p1==++p2
,更不能保证*(++p1)==*(++p2)
。因此,用输入迭代器读入的序列不能保证是可重复的。因此,输入流迭代器只适用于作为那些只需要遍历序列一次的算法的输入。
输出迭代器可以向序列中写入数据,但不一定能够向序列中读取数据。
输出迭代器也支持对序列进行不可重复的单向遍历。
输出流迭代器是一种典型的输出迭代器。
下面是在迭代器的通用功能之外,输出迭代器所具备的功能:
表达式 | 功能 | 返回值 |
---|---|---|
*p1=t |
向迭代器所指向位置写入一个元素 | 返回类型不确定 |
*p1++=t |
等价于{*p1=t; ++p1;} |
返回类型不确定 |
注意:使用输出迭代器,写入元素的操作和使用“++
”自增的操作必须交替进行。如果连续两次自增之间没有写入元素,或连续两次使用“*p1=t
”这样的语法写入元素之间没有自增,其行为都是不确定的。
前向迭代器这一概念是输入迭代器和输出迭代器这两个概念的子概念。
前向迭代器既支持数据读取,也支持数据写入。
前向迭代器支持对序列进行可重复的单向遍历。
下面是在输入迭代器和输出迭代器的功能之外,前向迭代器还具备的功能:
表达式 | 功能 | 返回值 |
---|---|---|
*p1 |
T& 类型 |
|
p1++ |
使迭代器指向下一个元素 | 返回类型为P,其值为{P p2=p1; ++p1; return p2;} |
注意:前向迭代器去掉了输入迭代器和输出迭代器这两个概念中的一些不确定性。对于前向迭代器,如果p1==p2
,那么++p1==++p2
一定成立,这意味着,前后两次使用相等的输入迭代器遍历一个序列,只要序列的值在这一过程中没有被改写,就一定会得到相同的结果,因此,前向迭代器对序列的遍历是可重复的。另外,前置迭代器不再有输出迭代器中关于“++
自增操作和对元素的写入操作必须交替进行”的限制。
双向迭代器这一概念是前向迭代器这一概念的子概念。
在前向迭代器所支持功能(数据读取、数据写入、对序列进行可重复的单向遍历)的基础上,双向迭代器又支持迭代器向反方向移动。
下面是在前向迭代器的功能之外,双向迭代器还支持的功能:
表达式 | 功能 | 返回值 |
---|---|---|
--p1 |
使迭代器指向上一个元素 | p1自身的引用 |
p1-- |
使迭代器指向上一个元素 | 返回类型为X,其值为{P p2=p1; --p1; return p2;} |
随机访问迭代器这一概念是双向迭代器这一概念的子概念。
在双向迭代器所支持功能(数据读取、数据写入、对序列进行可重复的单向遍历、迭代器向反方向移动)的基础上,,随机访问迭代器又支持直接将迭代器向前或向后移动n个元素。因此,随机访问迭代器的功能几乎和指针一样。
下面是在双向迭代器的功能之外,随机访问迭代器还支持的功能:
表达式 | 功能 |
---|---|
p1+=n |
将迭代器p1向前移动n个元素 |
p1-=n |
将迭代器p1向后移动n个元素 |
p1+n 或n+p1 |
获得指向迭代器p1前第n个元素的迭代器 |
p1-n |
获得指向迭代器p1后第n个元素的迭代器 |
p1-p2 |
返回一个满足p1==p2+n 的整数n |
p1 op p2 |
这里的op 可以是<,<=,>,>=,用于比较p1和p2所指位置的前后关系,等价于p1-p2 op 0 |
p1[n] |
等价于*(p1+n) |
通过向量容器vector的begin和end函数得到的迭代器就是随机访问迭代器。
指针也是随机访问迭代器。
STL算法的形参中常常包括一对输入迭代器,用它们所构成的区间来表示输入数据的序列。例如在10.2.1小节中介绍过的transform
算法,它的前两个参数”istream_iterator
“就构成了一个区间。
设p1和p2是两个输入迭代器,以后将使用**[p1,p2)形式来表示它们所构成的区间**。这样一个区间是一个有序序列,包括p1和p2两个迭代器所指向元素之间的所有元素但不包括p2所指向的元素。
当p1==p2时,[p1,p2)是一个没有任何元素的空区间。
并非任何两个迭代器都能确定一个合法的区间,比如如果p1和p2指向的是不同容器中的元素,或者它们虽指向同一容器中的元素,但p1>p2,那么[p1,p2)就不是一个合法的区间。当且仅当对p1执行n次(n≥0)”++“运算后,表达式p1==p2的值为true,[p1,p2)才是一个合法的区间。
在设计一个算法时,如果用迭代器作为参数,应当尽量用内涵尽可能小、外延尽可能大的迭代器概念,这样适用范围最广。
STL为迭代器提供了两个辅助函数模板——advance和distance。
advance函数模板的原型是:
template //advance函数模板的原型
void advance(InputIterator& iter, Distance n);
advance函数模板用来使迭代器iter
前进n
个元素。
对于双向迭代器和随机访问迭代器(这两种迭代器均支持反向移动),n
可以取负值,表示让iter
后退n
个元素。
对于一个随机访问迭代器iter
,执行advance(iter, n)
就相当于执行了iter+=n
。
distance函数模板的原型是:
template //distance函数模板的原型
unsigned distance(InputIterator first, InputIterator last);
distance函数模板用来计算first
经过多少次”++
“运算后可以到达last
,[first, last)必须是一个有效的区间。
若first
和last
皆为随机访问迭代器,在**[first, last)
必须是一个有效的区间的前提下,distance(first, last)
的值等于last-first
**。