标准库定义了三种顺序容器类型:vector、list、deque(双端队列)。他们的差别在于访问元素的方式,以及添加或删除元素相关操作的运行代价。标准库还提供了三种容器适配器,适配器是根据原始的容器类型所提供的操作。顺序容器适配器包括stack、queue、priority_queue。
顺序容器
vector:支持快速随机访问
list:支持快速插入/删除
deque:双端队列
顺序容器适配器
stack:后进先出栈
queue:先进先出队列
priority_queue:有优先级管理的队列
9.1 顺序容器的定义
头文件:
#include <vector>
#include <list>
#include <deque>
要定义某种特殊的容器,必须在容器名后面加一对尖括号,尖括号里面提供容器中存放的元素的类型:
vector<string> svec;
list<int> ilist;
deque<Sales_item> items;
容器内元素的类型约束:元素类型必须支持赋值运算;元素类型的对象必须可以复制。
9.2 迭代器
C++语言使用一对迭代器标记迭代器范围,这两个迭代器分别指向同一个容器中的两个元素或超出末端的下一个位置,通常将它们命名为first和last,或beg和end。第二个迭代器从来都不是指向元素范围的最后一个元素,而是指向最后一个元素的下一个位置。该范围内的元素包括迭代器first指向的元素,以及从first开始一直到迭代器last指向的位置之前的所有元素。如果两个迭代器相等,则迭代器范围为空。
此类元素范围称为左闭合区间:[ first, last ),表示范围从first开始,到last结束,但不包括last。迭代器last可以等于first,或者指向first标记的元素后面的某个元素,但绝对不能指向first标记的元素前面的元素。
9.3 顺序容器的操作
每种容顺序容器都提供了一组有用的类型定义以及以下操作:
- 在容器中添加元素
- 在容器中删除元素
- 设置容器大小
- (如果有)获取容器的第一个和最后一个元素
9.3.1 容器定义的类型别名
iterator:此容器类型的迭代器类型
reverse_iterator:按逆序寻址元素的迭代器,逆序迭代器从后向前遍历容器,并反转了某些相关的迭代器操作:例如,在逆序迭代器上做++运算将指向容器中的前一个元素。
使用容器定义类型的表达式看上去非常复杂:
list<string>::iterator iter;
iter的声明使用了作用域操作符,以表明此时所使用的符号::右边的类型名字是在符号::左边指定容器的作用域内定义的。其效果是将iter声明为iterator类型,而iterator是存放string类型元素的list类的成员。
9.3.2 begin和end成员
begin和end操作产生指向容器内第一个元素和最后一个元素的下一个位置的迭代器。这两个迭代器通常用于标记包含容器中所有元素的迭代器范围。
c.begin() 返回一个迭代器,指向容器c的第一个元素
c.end() 返回一个迭代器,指向容器c的最后一个元素的下一位置
c.rbegin() 返回一个逆序迭代器,指向容器c的最后一个元素
c.rend() 返回一个逆序迭代器,指向容器c的第一个元素前面的位置
9.3.3 在顺序容器中添加元素
c.push_back(t) 在容器c的尾部添加值为t的元素,返回void类型
c.push_front(t) 在容器c的前端添加值为t的元素,返回void类型
只适用于list和deque容器类型
c.insert(p, t) 在迭代器p所指向的元素前面插入值为t的新元素,返回指向新添加元素的迭代器
c.insert(p, n, t) 在迭代器p所指向的元素前面n个插入值为t的新元素,返回void类型
c.insert(p, b, e) 在迭代器p所指向的元素前面插入由迭代器b和e标记的范围内的元素,返回void类型
不要存储end操作返回的迭代器。添加或删除deque或vector容器内的元素都会导致存储的迭代器失效。
9.3.5 容器大小的操作
c.size() 返回容器c中的元素个数,返回类型为c::size_type c.max_size() 返回容器c可容纳的最多元素个数,返回类型为c::size_type c.empty() 返回标记容器大小是否为0的布尔值 c.resize(n) 调整容器c的长度大小,使其能容纳n个元素。如果n<c.size(),则删除多出来的元素;否则,添加采用值初始化的新元素 c.resize(n, t) 调整容器c的长度大小,使其能容纳n个元素,所有新添加的元素值都为t
9.3.6 访问元素
c.back() 返回容器c的最后一个元素的引用,如果c为空,则该操作未定义 c.front 返回容器c的第一个元素的引擎,如果c为空,则该操作未定义 c[n] 返回下标为n的元素的引用,如果n<0或者n>c.size(),则该操作未定义。只适用于vector和deque容器 c.at(n) 返回下标为n的元素的引用。如果下标越界,则该操作未定义。只适用于vector和deque容器
9.3.7 删除元素
c.erase(p) 删除迭代器p指向的元素。返回一个迭代器,它指向被删除元素后面的元素。如果p指向容器内的最后一个元素,则返回的迭代器指向容器的超过末端的下一位置。
如果p本身就是指向超出末端的下一个位置的迭代器,则该函数未定义
c.erase(b, e) 删除迭代器b和e所标记的范围内的所有元素。返回一个迭代器,指向被删除元素段后面的元素,如果e本身就是指向超出末端的下一位置,则返回的迭代器也指向超出末端的下一位置。
c.clear() 删除容器c内的所有元素,返回void
c.pop_back() 删除容器c的最后一个元素。返回void,如果c为空容器,则该函数未定义
c.pop_front 删除容器c的第一个元素,返回void,如果c为空,则该函数未定义。只适用于list或deque容器
9.3.8 赋值与swap
c1 = c2 删除容器c1的所有元素,然后将c2的元素复制给c1。c1和c2的类型(包括容器类型和元素类型)必须相同
c1.swap(c2) 交换内容:调用完该函数后,c1中存放的是c2原来的元素,c2中存放的是c1原来的元素。c1和c2的类型必须相同。
该函数的执行速度通常要比将c2的元素复制到c1的操作快
c.assign(b, e) 重新设置c的元素:将迭代器b和e标记的范围内所有的元素复制到c中。b和e不是指向c中元素的迭代器
c.assign(n ,t) 将容器c重新设置为存储n个值为t的元素
9.4 vector容器的自增长
为了支持快速随机访问,vector容器的元素是以联系的方式存放的,每一个元素都紧挨着前一个元素存储。当在容器中添加元素时,如果已经没有空间容纳新的元素,由于元素必须是连续存储,所以不能随便找个地方存储新元素。于是,vector必须重新分配存储空间,用来存放原来的元素以及新添加的元素:存放在旧存储空间中的元素被复制到新的存储空间,接着插入新元素,最后撤销旧的存储空间。
而对于不连续存储元素的容器,比如list,就不存在这样的内存分配问题。只需要创建一个新的元素,然后将该元素连接在已经存在的链表中。因此推论:一般而言,使用list容器优于vector容器。
为了使vector实现快速的内存分配,其实际分配的容量要比当前所需的空间多一些。vector容器预留了这些额外的存储区,用于存放新添加的元素。于是,不必为每个新元素重新分配容器。所分配的额外内存容量因库的实现不同而不同,这种策略会带来显著的效率。事实上,其性能非常好,因此在实际应用中,比起list和deque,vector的增长效率通常会更高。
vector类提供两个成员函数:capacity和reserve。capacity操作获取在容器需要分配更多的存储空间之前能够存储的元素总数,而reserve操作则告诉vector容器应该预留多少个元素的存储空间。每当vector容器不得不重新分配新的存储空间时,以加倍当前容量的分配策略实现重新分配。
9.5 容器的选用
分配连续存储元素的内容空间会影响内存分配策略和容器对象的开销。通过巧妙的实现技巧,标准库的实现者已经最小化了内存分配的开销。元素是否连续存储还会显著的影响:1.在容器中的中间位置添加或删除元素的代价 2.执行容器元素的随机访问的代价。使用这些操作的程度将决定应该选择哪种类型的容器。
vector和deque容器提供了对元素的快速随机访问,但付出的代价是,在容器的任意位置插入或删除严肃,比在容器的尾部插入和删除元素的开销更大。list类型在任何位置都能快速插入和删除,但付出的代价是元素的堆积访问开销较大。
选择容器的提示:
(1)如果程序要求随机访问元素,则应使用vector或deque容器
(2)如果程序必须在容器的中间位置插入或删除元素,则应采用list容器
(3)如果程序不是在容器的中间位置,而是在容器首部或者尾部插入或删除元素,则应该采用deque容器
(4)如果只需在读取输入时在容器的中间位置插入元素,然后需要随机访问元素,则可考虑在输入时将元素读到一个list容器,接着对容器重新排序,使其适合顺序访问,然后将排序后的list容器复制到一个vector容器。
如果无法确定某种应用应该采用哪种容器,则编写代码时可以尝试只使用vector和list容器都提供的操作:使用迭代器,而不是下标,并且避免随机访问元素。这样编写代码,在必要时,可很方便地将程序从使用vector容器修改为使用list容器。
9.7 容器适配器
除了顺序容器,标准库还提供了三种顺序容器适配器,queue、priority_queue和stack。适配器(adapter)是标准库中通用的概念,包括容器适配器、迭代器适配器和函数适配器。本质上,适配器是使一种事物的行为类似于另一事物的一种机制。容器适配器让一种已存在的容器类型采用另一种不同的抽象类型的工作方式实现。例如,栈适配器可使任何一种顺序容器以栈的方式工作。
9.7.1 栈适配器
栈适配器所支持的操作:
s.empty() 如果栈为空,则返回true,否则,返回false s.size() 返回栈中元素的个数 s.pop() 删除栈顶元素,但不返回其值 s.top() 返回栈顶元素的值,但不删除该元素 s.push(item) 在栈顶压入新元素
所有容器适配器都根据其基础容器所支持的操作来定义自己的操作。默认情况下,栈适配器建立在deque的基础上,因此采用deque提供的操作来实现栈功能。例如,push操作通过调用push_back操作实现,而该push_back操作由stack所基于的deque对象提供。尽管栈是以deque容器为基础实现的,但是程序员不能直接访问deque所提供的操作。
9.7.2 队列和优先级队列
标准库队列使用了先进先出(FIFO)的存储和检索策略。进入队列的对象被放置在尾部,下一个被取出的元素则取自队列的首部。标准库提供了两种风格的队列:FIFO队列(FIFO queue,简称queue)以及优先级队列(priority queue)。
priority queue允许用户为队列中存储的元素设置优先级,这种队列不是直接将新元素放在队列尾部,而是放在比它优先级低的元素前面。实例比如机场行李检查队列,程序示例如操作系统的调度表。
要使用这两种队列必须包含queue头文件。
q.empty() 如果队列为空,则返回true,否则返回false
q.size() 返回队列中元素的个数
q.pop() 删除队首元素,但不返回其值
q.front() 返回队首元素的值,但不删除该元素。该操作只适用于队列
q.back() 返回队尾元素的值,但不删除该元素。该操作只适用于队列
q.top() 返回具有最高优先级的元素值,但不删除该元素。该操作只适用于优先级队列
q.push(item) 对于queue,在队尾压入一个新元素;对于priority queue,在基于优先级的适当位置插入新元素
小结
C++标准库定义了一些列顺序容器类型。容器是用于存储某种给定类型对象的模板类型。在顺序容器中,所有元素根据其位置排列和访问。所有容器都提供动态内存管理,程序员在容器中添加元素时,不必操心元素存放在哪里。容器自己实现其存储管理。
最近常使用的容器类型是vector,它支持对元素的快速随机访问。可高效地在vector容器尾部添加和删除元素,而在其他任何位置上的插入或删除运算则要付出比较昂贵的代价。deque与vector相似,但它还支持在deque首部的快速插入和删除运算。list类只支持元素的顺序访问,但在list内部任何位置插入和删除元素都非常快速。