STL提供了一组表示容器、迭代器、函数对象和算法的模板。容器是一个与数组类似的单元,可以存储若干个值。STL容器是同质的,即存储的值的类型相同:算法是完成特定任务(如对数组进行排序或在链表中查找特定值)的处方。迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,是广义指针。函数对象是类似于函数的对象,可以是类对象或函数指针(包括函数名,因为函数名被用作指针)。STL使得能够构造各种容器(包括数组、队列和链表)和执行各种操作(包括搜索、排序和随机排列)。
Alex Stepanov和 Meng Lee在Hewlett-Packard实验室开发了STL,并于1994年发布其实现。ISO/ANSIC++委员会投票同意将其作为C++标准的组成部分。STL 不是面向对象的编程,而是一种不同的编程模式–泛型编程(generic programming)。这使得STL 在功能和方法方面都很有趣。关于STL的信息很多,无法用一章的篇幅全部介绍,我们也只是简要的使用。
要创建vector模板对象,可使用通常的
# include
int main() {
vector<int> v(5);
}
由于运算符[]被重载,因此创建vector对象后,可以用通常的数组表示法来访问各个元素:
cout << v[3] << endl;
与string 类相似,各种STL容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器对象来管理内存。例如,vector模板的开头与下面类似:
template <class T,class Allocator = allocator<T>>
class vector {...}
如果省略该模板参数的值,则容器模板将默认使用allocator
size()
返回容器中元素的数目
swap()
交换两个容器的内容
begin()
返回一个指向容器中第一个元素的迭代器
end()
返回一个表示超过容器尾的迭代器
push_back()
将元素添加到矢量末尾
erase()
删除矢量中给定区间内的元素,接受迭代器参数
insert()
插入元素,第一个参数指定插入位置,第二个和第三个指定了被插入区间
什么是迭代器呢?他是一个广义指针,事实上,他可以是指针,也可以是一个可对其执行类似指针操作----如解除引用(operator*())和递增(operator++())的对象。稍后知道,通过将指针广义化为迭代器,让STL能够为各种不同的容器类(包括那些简单质证无法处理的类)提供统一接口,每个容器类都定义了一个合适的迭代器,该迭代器的类型是一个名为iterator的typedef,其作用域为整个类。例如,要为vector的double类型规范声明一个迭代器,可以这样做:
vector<double>::iterator pd;
假设scores是一个vector
vector<double> scores;
则可以使用迭代器pd执行这样的操作:
pd = scores.begin();
*pd = 22.3;
++pd;
正如您所见,迭代器的行为就像指针。还有一个C++11自动类型推断很有用的地方,例如,可以不这样做
vector<double>::iterator pd = scores.begin();
而这样做
auto pd = scores.begin();
回到前面的示例。什么是超过结尾(past-the-end)呢?它是一种迭代器指向容器最后一个元素后面的那个元素。这与C风格字符串最后一个字符后面的空字符类似,只是空字符是一个值,而“超过结尾”说设置为是一个指向元素的指针(迭代器)。end( )成员函数标识超过结尾的位置。如果将迭代器设置为容器的第一个元素,并不断地递增,.则最终它将到达容器结尾,从而遍历整个容器的内容。因此,如果scores和pd的定义与前面的示例中相同,则可以用下面的代码来显示容器的内容
for(pd = scores.begin();pd != scores.end();pd++){
cout << *pd << endl;
}
scores.erase(scores.begin(),scores.begin()+2);
vector<int> old;
vector<int> new;
old.insert(old.begin(),new.begin()+1,new.end());
程序员通常要对数组执行很多操作,如搜索、排序、随机排序等。矢量模板类包含了执行这些常见的操作的方法吗?没有! STL 从更广泛的角度定义了非成员(noni-member厂函数来执行这些操作,即不是为每个容器类定义find( )成员函数,.而是定义了一个适用于所有容器类的非成员函数 find()。这种设计理念省去了大量重复的工作。例如,假设有8个容器类,需要支持10种操作。如果每个类都有自己的成员函数,则需要定义80(8*10)个成员函数。但采用STL方式时,只需要定义10个非成员函数即可。在定义新的容器类时,只要遵循正确的指导思想,则它也可以使用已有的10个非成员函数来执行查找、排序等操作。
另一方面,即使有执行相同任务的非成员函数,STL有时也会定义一个成员函数。这是因为对有些操作来说,类特定算法的效率比通用算法高。因此,vector 的成员函数swap()的效率比非成员函数swap()高,但非成员函数让您能够交换两个类型不同的容器的内容。
三个具有代表性的STL函数:for_each()、random_shuffle()、sort()。
**for_each()**函数可以用于很多容器类,它接受三个参数,前两个是定义容器中区间的迭代器,最后一个是指向函数的指针(更普遍的说,最后一个参数是一个函数对象)。for_each()函数被指向的函数应用于容器区间中的各个元素。被指向的函数不能修改容器元素的值。可以用for_each()来代替for循环
vector<Review>::iterator pr;
for (pr = books.begin();pr!=books.end();pr++){
ShowReview(*pr);
}
替换为
for_each(books.begin(),books.end(),showReview);
这样可以避免显式的使用迭代器变量。
**random_shuffle()**函数接受两个指定区间的迭代器参数,并随即排列该杜建中的元素。
random_shuffle(books.begin(),books.end());
与可用于任何容器类的for_each不同,该函数要求容器类允许随机访问。
**sort()**函数也要求容器支持随机访问,该函数有两个版本,第一个版本接受两个定义区间的迭代器参数并使用为存储在容器中的类型元素定义的<运算符,对区间中的元素进行操作。例如,下面的语句按升序对coolstuf的内容进行排序,排序时使用内置的<运算符对值讲行比较:
vector<int> coolstuff;
sort(coolstuff.begin(),coolstuff.end());
如果容器元素是用户定义的对象,则要使用sort(),必须定义能够处理该类型对象的operator<()函数。例如,如果为Review提供了成员或非成员函数operafor<(),则可以对包含Review对象的矢量进行排序。由于 Review是一企结构,因此其成员是公有的,这样的非成员函数将为:.
bool operator<(const Review &r1,const Review &r2){
if(r1.title < r2.title) {
return true;
} else {
return false;
}
}
有了这样的函数后才可以排序。
基于范围的for循环是为了用于STL而设计的,我们复习一下用法
double prices[5] = {1.1, 1.2, 1.3};
for(double x : prices) {
cout << x << endl;
}
那我们其实也可以这么写
for (auto book:books) {
cout << book.title << endl;
}
有了一些使用STL的经验后,来看一些底层理念,STL是一种泛型编程。面向对象编程关注的是编程的数据方面。而泛型编程关注的是算法。他们之间的共同点是抽象和创建可重用代码,但是他们的理念不同。
泛型编程旨在编写独立于数据类型的代码。在C++中,完成通用程序的工具是模板。当然,模板使得能够按泛型定义函数或类,而STL通过通用算法更进了一步。模板让这一切成为可能,但必须对元素进行仔细地设计。为了解模板和设计是如何协同工作的,来看一看需要迭代器的原因。
理解迭代器是理解STL的关键所在。模板使得算法独立于存储的数据类型,而迭代器使算法独立于使用的容器类型。因此,它们都是STL通用方法的重要组成部分。
为了解为何需要迭代器,我们来看如何为两种不同数据表示实现find函数,然后来看如何推广这种方法。首先看一个在 double数组中搜索特定值的函数,可以这样编写该函数:
double *find(double *ar,int n,const double & val){
for(int i=0;i<n;i++) {
if (ar[i] == val) {
return &ar[i];
}
}
return 0;
// return nullptr;
}
这种find方法与一种特定的数据结构捆绑在一起了,我们可以试一下寻找链表中的某个数据
struct Node {
double item;
Node *next;
};
Node *find(Node *head,const double &val) {
Node *start;
for(start = head;start!=NULL;start=start.next){
if(start->item == val) {
return start;
}
}
return nullptr;
}
从广义上讲,这两种算法是相同的,将值依次与容器中的每个值进行比较,直到找到匹配值为止。
泛型编程旨在使用同一个find函数来处理数组、链表或任何其他容器类型。即函数不仅独立于容器中存储的数据类型,而且独立于容器本身的数据结构-模板提供了存储在容器中的数据类型的通用表示,因此还需要遍历容器中的值的通用表示,迭代器正是这样的通用表示。
要实现find函数,迭代器应具备那些特性呢
迭代器也可以完成其他的操作,但有上述功能就足够了,至少对于find函数是如此。实际上,STL按功能的强弱定义了多种级别的迭代器,这将在后面介绍。顺便说一句,常规指针就能满足迭代器的要求,因此,可以这样重新编写find_arr()函数。
在STL中,每个类(vector,list,deque)等都定义了相应的迭代器类型。于其中的某个类,迭代器可能是指针,而对于另一个类,则可能是对象。不管实现方式如何迭代器都将提供所需的操作,如*和++(有些类需要的操作可能比其他类多)。其次,每个容器类都有一个超尾标记,当迭代器递增到超越容器的最后一个值后,这个值将被赋给迭代器。每个容器类都有begin()和end( )方法,它们分别返回一个指向容器的第一个元素和超尾位置的迭代器。每个容器类都使用++操作,让迭代器从指向第一个元素逐步指向超尾位置,从而遍历容器中的每一个元素。
使用容器类时,无需知道其迭代器是如何实现的,也无需知道超尾是如何实现的,而只需知道它有迭代器,其 begin()返回一个指向第一个元素的迭代器,end( )返回一个指向超尾位置的迭代器即可。
使用C++的自动类型推断可进一步简化循环遍历程序
for(auto pr = scores.begin();pr!=scores.end();pr++){
cout << *pr << endl;
}
不同的算法对迭代器的要求也不同。例如,查找算法需要定义++运算符,以便迭代器能够遍历整个容器;它要求能够读取数据,但不要求能够写数据(它只是查看数据,而并不修改数据)。而排序算法要求能够随机访问,以便能够交换两个不相邻的元素。如果iter 是一个迭代器,则可以通过定义+运算符来实现随机访问,这样就可以使用像iter+10这样的表达式了。另外,排序算法要求能够读写数据。
STL定义了5种迭代器,并根据所需的迭代器类型对算法进行了描述。这5种迭代器分别是输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器。例如,find( )的原型与下面类似:
template <class InputIterator,class T>
InputIterator find(InputIterator first,InputIterator last,const T& value);
术语“输入”是从程序的角度说的,即来自容器的信息被视为输入,就像来自键盘的术语“输入”是从程序的角度说的,即来自容器的信息被视为输入,就像来自键盘的信息对程序来说是输入一样。因此,输入迭代器可被程序用来读取容器中的信息。具体地说,对输入迭代器解除引用将使程序能够读取容器中的值,但不一定能让程序修改值。因此,需要输入迭代器的算法将不会修改容器中的值。
输入迭代器必须能够访问容器中所有的值,这是通过支持++运算符(前缀格式和后缀格式)来实现的。如果将输入迭代器设置为指向容器中的第一个元素,并不断将其递增,直到到达超尾位置,则它将依次指向容器中的每一个元素。顺便说一句,并不能保证输入迭代器第二次遍历容器时,顺序不变。另外,输入迭代器被递增后,也不能保证其先前的值仍然可以被解除引用。基于输入迭代器的任何算法都应当是单通行(single-pass)的,不依赖于前一次遍历时的迭代器值,也不依赖于本次遍历中前面的迭代器值。
注意,输入迭代器是单向迭代器,可以递增,但不能倒退。
STL 使用术语“输出”来指用于将信息从程序传输给容器的迭代器,因此程序的输出就是容器的输入。输出迭代器与输入迭代器相似,只是解除引用让程序能修改容器值,而不能读取。也许您会感到奇怪,能够写,却不能读。发送到显示器上的输出就是如此,cout可以修改发送到显示器的字符流,却不能读取屏幕上的内容。STL足够通用,其容器可以表示输出设备,因此容器也可能如此。另外,如果算法不用读取作容器的内容就可以修改它(如通过生成要存储的新值),则没有理由要求它使用能够读取内容的迭代器。
简而言之,对于单通行、只读算法,可以使用输入迭代器;而对于单通行、只写算法,则可以使用输出迭代器。
与输入迭代器和输出迭代器相似,正向迭代器只使用++运算符来遍历容器,所以它每次沿容器向前移动一个元素;然而,与输入和输出迭代器不同的是,它总是按相同的顺序遍历一系列值。另外,将正向迭代器递增后,仍然可以对前面的迭代器值解除引用(如果保存了它),并可以得到相同的值。这些特征使得多次通行算法成为可能。
正向迭代器可以读取数据也可以修改数据
假设算法需要能够双向遍历容器,情况将如何呢﹖例如,reverse函数可以交换第一个元素和最后一个元素、将指向第一个元素的指针加1、将指向第二个元素的指针减1,并重复这种处理过程。双向迭代器具有正向迭代器的所有特性,同时支持两种(前缀和后缀)递减运算符。
有些算法(如标准排序和二分检索)要求能够直接跳到容器中的任何一个元素,这叫做随机访问,需要随机访问迭代器。随机访问迭代器具有双向迭代器的所有特性,同时添加了支持随机访问的操作(如指针增加运算)和用于对元素进行排序的关系运算符。
您可能已经注意到,迭代器类型形成了一个层次结构。正向迭代器具有输入迭代器和输出迭代器的全部功能,同时还有自己的功能;双向迭代器具有正向迭代器的全部功能,同时还有自己的功能;随机访问迭代器具有正向迭代器的全部功能,同时还有自己的功能。
STL有若干个用C++语言无法表达的特性,如迭代器种类。因此,虽然可以设计具有正向迭代器特征的类,但丕能让编译器将算法限制为只使用这个类。原因在于,正向迭代器是一系列要求,而不是类型。所设计的迭代器类可以满足这种要求,常规指针也能满足这种要求。STL 算法可以使用任何满足其要求的迭代器实现。STL-文献使用术语概念(concept)来描述–系列的要求。因眦,存在输入迭代器概念、正向迭代器概念,等等。顺便说一句,如果所设计的容器类需要迭代器,可考虑STL,它包含用于标准种类的迭代器模板。
STL具有容器概念和容器类型。概念是具有名称(如容器、序列容器、关联容器等)的通用类别;容器类型是可用于创建具体容器对象的模板。以前的11个容器类型分别是deque、list、queue、priority_queue、stack、vector、map、multimap、set、multiset和 bitset (本章不讨论bitset,它是在比特级处理数据的容器)。C++11新增了forward_list、unordered_map、unordered_multimap、unordered_set 和 unordered_multiset,且不将bitset视为容器,而将其视为一种独立的类别。因为概念对类型进行了分类,下面先讨论它们。
没有与基本容器概念对应的类型,但概念描述了所有容器类都通用的元素。它是一个概念化的抽象基类–说它概念化,是因为容器类并不真正使用继承机制。换句话说,容器概念指定了所有STL容器类都必须满足的一系列要求。
可以通过添加要求来改进基本的容器概念。序列( sequence)是一种重要的改进,七种STL容器类型都是序列,包括forward_list、list、queue、priority_queue、stack和 vector都是序列,序列允许您在队尾添加元素,在队手删除元素,deque表示的双端队列允许在两端添加和删除元素。array也被归为序列容器,虽然它并不满足所有的要求。
序列还要求其元素按严格的线性顺序排列,即存在第一个元素、最后一个元素,除第一个元素和最后一个元素外,每个元素前后都分别有一个元素。数组和链表都是序列,但分支结构(其中每个节点都指向两个子节点)不是。
vector
前面介绍了多个使用vector模板的例子,该模板是在vector头文件中声明的。简单地说,vector是数组的一种类表示,它提供了自动内存管理功能,可以动态地改变vector对象的长度,并随着元素的添加和删除而增大和缩小。它提供了对元素的随机访问。在尾部添加和删除元素的时间是固定的,但在头部或中间插入和删除元素的复杂度为线性时间。
vector 模板类是最简单的序列类型,除非其他类型的特殊优点能够更好地满足程序的要求,否则应默认使用这种类型。
deque
deque模板类(在deque头文件中声明)表示双端队列(double-ended queue),通常被简称为deque,在STL中,其实现类似于vector容器,支持随机访问,主要区别在于,从deque对象的开始位置插入和删除元素的时间是固定的,而不像vector中那样是线性时间的。所以,如果多数操作发生在序列的起始和结尾处,则应考虑使用deque数据结构。
list
list模板类(在list头文件中声明)表示双向链表。除了第一个和最后一个元素外,每个元素都与前后的元素相链接,这意味着可以双向遍历链表。list和vector之间关键的区别在于list在链表中任一位置进行插入和删除的时间都是固定的(vector模板提供了除结尾处外的线性时间的插入和删除,在结尾处,它提供了固定时间的插入和删除)。因此,vector 强调的是通过随机访问进行快速访问,而list强调的是元素的快速插入和删除。
与vector相似,list也是可反转容器。与vector不同的是,list不支持数组表示法和随机访问。与矢量迭代器不同,从容器中插入或删除元素之后,链表迭代器指向元素将不变。
list方法组成了一个方便的工具箱。例如,假设有两个邮件列表要整理,则可以对每个列表进行排序,合并它们,然后使用unique( )来删除重复的元素。
forword_list(C++11)
C++新增了容器类forward_list,它实现了单链表。在这种链表中,每个节点都只链接到下一个节点,而没有链接到前一个节点。因此_forward_list只需要正向迭代器,而不需要双向迭代器。因此,不同于vector和 list,forward-list是不可反转的容器。相比与list,forward_list 更简单、更紧凑,但功能也更少。
queue
queue模板的限制比 deque更多。它不仅不允许随机访问队列元素,甚至不允许遍历队列。它把使用限制在定义队列的基本操作上,可以将元素添加到队尾、从队首删除元素、查看队首和队尾的值、检查元素数目和测试队列是否为空。
priority_queue
priority_queue 模板类(在queue头文件中声明)是另一个适配器类,它支持的操作与queue相同。两者之间的主要区别在于,在priority _queue 中,最大的元素被移到队首(生活不总是公平的,队列也一样)。内部区别在于,默认的底层类是vector。可以修改用于确定哪个元素放到队首的比较方式,方法是提供一个可选的构造函数参数。
stack
stack是栈接口,stack模板的限制比vector更多。它不仅不允许随机访问栈元素,甚至不允许遍历栈。它把使用限制在定义栈的基本操作上,即可以将压入推到栈顶,从栈顶弹出元素,查看柱顶的值、检查元素数目和测试栈是否为空。
array(C++11)
模板类array是头文件array中定义的,他并非STL容器,因为长度是固定的,因此,array没有定义调整容器大小的操作。但是定义了对他来讲有意义的成员函数,如operator和at[]。
关联容器通常是使用某种树实现的。树是一种数据结构,其根节点链接到一个或两个节点,而这些节点又链接到一个或两个节点,从而形成分支结构。像链表一样,节点使得添加或删除数据项比较简单。但相对于链表,树的查找速度更快。
STL提供了四种关联容器:set,multiset,map,multimap,前两个是在头文件set中定义的,后两个是在头文件map中定义的。
最简单的关联容器是set,其值类型与键相同,键是唯一的,这意味着集合中不会有多个相同的键。确实,对于set来说,值就是键。multiset类似于set,只是可能有多个值的键相同。例如,如果键和值的类型为int,则multiset对象包含的内容可以是I、2、2、2、3、5、7、7。
在map中,值与键的类型不同,键是唯一的,每个键只对应一个值。multimap 与 map相似,只是一个键可以与多个值相关联。
set
STL set模拟了多个概念,它是关联集合,可反转,可排序,且键是唯一的,所以不能存储多个相同的值。与vector和 list 相似,set也使用模板参数来指定要存储的值类型:
set A;
multimap
与set相似,multimap也是可反转的、经过排序的关联容器,但键和值的类型不同,且同一个键可能与多个值相关联。基本的 multimap声明使用模板参数指定键的类型和存储的值类型。例如,下面的声明创建一个multimap对象,其中键类型为int,存储的值类型为string:
multimap codes;
第三个模板参数是可选的,指出用于对键进行排序的比较函数或对象,默认情况下将使用模板less<>
插入数据
创建一个pair再将它插入
pair<const int ,string> item(213,"Los Angeles");
codes.insert(item);
也可以使用一条语句创建匿名pair对象并插入
codes.insert(pair<int ,string> (213,"Los Angeles"));
对于pair对象,可以使用first和second成员来访问其两个部分了。
cout << item.first << ' ' << item.second << endl;
无序关联容器是对容器概念的另一种改进。与关联容器一样,无序关联容器也将值与键关联起来,并使用键来查找值。但底层的差别在于,关联容器是基于树结构的,而无序关联容器是基于数据结构哈希表的,这旨在提高添加和删除元素的速度以及提高查找算法的效率。有4种无序关联容器,它们是unordered_set、unordered_map、unordered_multiset、unordered_multimap。
很多STL 算法都使用函数对象—-—也叫函数符( functor)。函数符是可以以函数方式与()结合使用的任意对象。这包括函数名、指向函数的指针和重载了()运算符的类对象(即定义了函数 operator()()的类)。例如,可以像这样定义一个类:
class Liner{
private:
double slope;
double y0;
public:
Liner(double s_ = 1,double y_ = 0):slope(s_),y0(y_){}
double operator()(double x) {return y0 + slope * x;}
};
这样重载的()运算符将使得能够像函数那样使用Liner对象。
Liner f1;
Liner f2(2.5,10.0);
double y1 = f1(12.5);
double y2 = f2(0.4);
还记得函数for_each嘛,他将指定的函数用于区间中的每个成员
for_each(books.begin(),books.end(),showReview);
通常第三个参数既可以是常规函数,也可以是函数福。实际上,这提出了一个问题,如何声明第三个参数呢?不能把他声明为函数指针,因为函数指针指定了参数类型,由于容器可以包含任意类型,所以预先无法知道应使用那种参数类型。STL通过使用模板解决了这个问题。for_each的原型看上去就像这样。
template <class InputIterator,class Function>
Function for_each(InputIterator first, InputIterator last,Function f);
showReview()原型如下:
void showReview(const Review &);
这样,标识符showReview的类型将为void(*)(const Review &),这也是赋给模板参数Function的类型。对于不同的函数调用,Function参数可以表示具有重载的()运算符类类型。
例如,提供给for_each()的函数符应当是一元函数,因为它每次用于一个容器元素。当然,这些概念都有相应的改进版。
一些STL函数需要谓词参数或者二元谓词参数。
STL定义了多个基本函数符,它们执行诸如将两个值相加、比较两个值是否相等操作。提供这些函数对象是为了支持将函数作为参数的STL函数。例如,考虑函数transform()它有两个版本。第一个版本接受4个参数,前两个参数是指定容器区间的迭代器(现在您应该已熟悉了这种方法),第3个参数是指定将结果复制到哪里的迭代器,最后一个参数是一个函数符,它被应用于区间中的每个元素。