顺序容器的元素按照其位置存储和访问。除了顺序容器之外,标准库还提供了几种关联容器,其元素按照键(key)排序。每组容器都提供一组不同的时间和功能的折中方案。顺序容器根据位置来存储和访问元素,元素的排列次序与元素值无关,而是由元素添加到容器的顺序决定。标准库定义了三种顺序容器:vector、list、dequeue。他们的差别在于元素访问的方式以及添加和删除元素相关操作的运行代价。标准库还提供了三种适配器。适配器是根据原始的容器类型所提供的操作,通过定义新的操作接口来适应基础的容器类型。顺序容器适配器包括stack、queue、priority_queue.
9.1 顺序容器的定义
所有的容器类型都定义了
默认构造函数,用于创建指定类型的
空容器对象。在大多数程序中使用默认构造函数能够达到最佳性能。
9.1.1 容器元素的初始化
除了默认构造函数,容器类型还定义了其他构造函数:
C
c;
C
c(c2);
C
c(b, e);
以上三种是
所有的容器都有的构造函数。
还有两种是顺序容器所特有的:
C
c(n, t);
C
c(n);
1. 将一个容器初始化为另一个容器的副本: 此时类型必须匹配(容器类型和元素类型都必须完全一致)。
2. 初始化为一段元素的副本:使用迭代器,此时不要求容器类型完全相同,元素类型也可以不同,只要他们兼容即可,能够将要复制的元素转换为所构建的新容器的元素类型即可实现复制。用这种方式
可以复制不能直接复制的容器。使用数组指针也可以初始化容器。
3. 分配和初始化指定数目的元素:可以显示指定容器大小和一个(可选)元素初始化式。如果
不提供元素初始化式,标准库将为容器实现值初始化,
元素类型必须是内置类型或复合类型,或者是提供了默认构造函数的类类型。
4. 关联容器不接受容器大小做形参的构造函数。
9.1.2 容器内元素的类型约束
容器内元素类型必须满足以下两个约束:
a、 元素类型必须支持
赋值运算;
b、 元素类型的对象必须可以
复制;
引用不支持一般意义的赋值运算,IO库类型不支持复制和赋值,因此这两种不能作为容器元素类型。
1. 容器操作的特殊要求:支持赋值和复制是最低要求。一些容器操作还有特殊的要求,如果不满足要求则只能定义容器而不能执行操作。例如:当容器存储类类型对象时,只有类类型提供默认构造函数才可以使用指定容器大小的初始化方式。
2. 容器的容器:可以定义元素是容器类型的容器:两个尖括号要分开。
9.2 迭代器和迭代器范围
迭代器为所有标准库容器类型提供的运算:
*iter
iter->mem
++iter
iter++
--iter
iter--
iter1 == iter2
iter1 != iter2
注:比较两个迭代器相等或不等,只有当两个迭代器都指向
同一个容器中的同一个元素或者指向同一个容器中的超出末端下一位置时才是相等的。
vector和dequeue容器的迭代器提供额外的运算:
迭代器算数运算
iter + n
iter - n
iter += n
iter -= n
iter1 - iter2
>, >=, <, <=
必须指向同一个容器
关系操作符只用于vector和dequeue的迭代器,list的迭代器没有关系运算符。
9.2.1 迭代器范围
迭代器first和last形成
迭代器范围的迭代器的要求:
- 他们指向同一个容器中的元素或者超出末端的下一位置。
- 如果两个迭代器不相等,则对first反复自增运算,必须能够到达last。
9.2.2 使迭代器失效的容器操作
一些操作会修改容器的内在状态或移动容器内的元素,这样的操作会使所有指向被移动的元素的迭代器失效,也可能使其他的迭代器失效。使用迭代器时,通常可以编写程序使得要求
迭代器有效的代码范围相对较短。
9.3 顺序容器的操作
每种容器都提供了一组有用的类型定义以及以下操作:
- 在容器中添加元素;
- 在容器中删除元素;
- 设置容器的大小;
- 获取容器内的第一个和最后一个元素。
9.3.1 容器定义的类型别名
size_type |
无符号整型,足以存储此容器类型的最大可能容器长度 |
iterator |
此容器类型的迭代器类型 |
const_iterator |
元素的只读迭代器类型 |
reverse_iterator |
按逆序寻址元素的迭代器 |
const_reverse_iterator |
元素的只读(不能写)逆序迭代器 |
difference_type |
足够存储两个迭代器差值的有符号整型,可为负数 |
value_type |
元素类型 |
reference |
元素的左值类型,是 value_type& 的同义词 |
const_reference |
元素的常量左值类型,等效于 const value_type& |
9.3.2 begin和end成员
begin和end分别产生指向容器第一个元素和最后一个元素的下一个位置的迭代器。
rbegin和rend分别产生指向最后一个元素和第一个元素前面的位置的迭代器。
9.3.3 在顺序容器中添加元素
所有的顺序容器都支持以
push_back操作在容器尾部插入一个元素,新插入的元素是插入元素的副本。
在顺序容器中添加元素的操作
c.push_back(t) |
在容器c尾部添加值为t的元素,返回void类型 |
c.push_front(t) |
在容器c的前端添加值为t的元素,返回void。只适用于list和dequeue |
c.insert(p, t) |
在迭代器p之前插入t元素,返回指向新添加元素的迭代器 |
c.insert(p, n, t) |
在迭代器p之前插入n个t,返回void |
c.insert(p, b, e) |
在迭代器p之前插入[b, e)范围的元素,返回void |
1.
容器元素都是副本: 在容器中添加元素时,是将元素值
复制到容器里。容器里存放的是原始元素的副本。
2. 添加元素可能会使迭代器失效:任何insert和push操作都可能使迭代器失效。
3. 避免存储end操作返回的迭代器:在容器的任何位置插入任何元素都会使该迭代器失效。
9.3.4 关系操作符
所有的容器类型都支持用
关系操作符来实现两个容器的比较。比较的容器必须具有
相同的容器类型以及元素类型。容器的比较是基于元素的比较,如果容器的元素不支持某种操作符,那么容器类型就不能做这种比较运算。使用元素提供的关系操作符来实现容器的关系运算。
9.3.5 容器大小的操作
所有的顺序容器类型都提供四种与容器大小有关的操作:
顺序容器的大小操作
c.size() |
返回c中元素个数,类型c::size_type |
c.max_size() |
返回c中可容纳的最多元素个数,类型为c::size_type |
c.empty() |
返回标记容器大小是否为0的布尔值 |
c.resize(n) |
调整容器大小,容纳n个元素,元素多了就删除,少了就添加,值初始化新元素 |
c.resize(n,t) |
调整容器大小,容纳n个元素,新添加的元素初始化为t |
9.3.6 访问元素
如果容器非空,front和back成员将发挥容器内第一个或最后一个元素的
引用。
访问顺序容器内元素的操作
c.back() |
返回容器c的最后一个元素的引用,c为空则操作未定义 |
c.front() |
返回容器c第一个元素的引用,为空则未定义 |
c[n] |
返回下表为n的元素的引用,只适用于vector和dequeue |
c.at(n) |
返回下标为n的元素的引用,只用于vector和dequeue |
9.3.7 删除元素
容器类型提供了通用的earse操作和特定的pop_front和pop_back操作删除元素。
删除顺序容器内元素的操作
c.erase(p) |
删除迭代器p所指元素,返回迭代器,指向被删除元素后面的元素 |
c.erase(b, e) |
删除[b, e),返回指向被删除元素段后面的元素的迭代器 |
c.clear() |
删除所有元素,返回void |
c.pop_back() |
删除c的最后一个元素,返回void |
c.pop_front() |
删除第一个元素,返回void,只适用于list和dequeue |
9.3.8 赋值与swap
与赋值相关的操作符都作用于整个容器。除swap外,其他操作都可以用erase和insert操作实现。赋值操作符首先删除做操作数容器中的所有元素,然后将右操作数容器的所有元素插入到左边容器中。
c1 = c2;
//等价于如下代码
c1.earse(c1.begin(), c1.end());
c1.insert(c1.begin(), c2.begin(), c2.end());
赋值和assign操作使
所有迭代器都失效,但是
swap操作不会使迭代器失效,完成swap以后尽管元素已经放在另一个容器中,但是
迭代器仍然指向相同的元素。
顺序容器的赋值操作
c1 = c2 |
删除c1的所有元素,将c2的元素复制给c1。c1和c2类型必须相同 |
c1.swap(c2) |
交换内容,c1和c2内容必须相同。执行速度比复制要快。 |
c.assign(b, e) |
重新设置c的元素为[b, e),b和e必须不是指向c中元素的迭代器,类型可以不完全一样。 |
c.assign(n, t) |
将容器c重新设置为n个值为t的元素。 |
1. 使用assign操作首先删除所有元素,然后插入新元素。如果容器类型相同元素类型相同,可以使用赋值操作符。否则的话,就必须使用assign函数。
2. swap操作节省了删除元素的成本。该操作
不会删除或插入任何元素,保证在
常量时间内完成。
9.4 vector容器的自增长
为了支持快速的随机访问,vector容器以连续的方式存放元素。虽然有时候插入新元素会导致重新分配内存空间并将旧空间里的元素复制到新的存储空间中,但是连续存储带来的访问便利弥补了这种操作的代价。为了使vector容器实现快速的内存分配,其实际分配的容量要比当前所需的空间多一些,vector预留了一部分的存储区用于存放新添加的元素。这样不必每次新插入元素的时候都要重新分配内存。所分配的额外内存的确切数因库的实现不同而不同。事实上,其性能非常好,因此在实际中,比起list和deque容器,vector的增长效率通常会更高。
capacity成员函数使程序员可以与vector容器的内存分配的实现部分交互工作。capacity获取容器在分配更多空间之前能够存储的元素总数。而reserve操作告诉vector容器应该预留多少个元素的存储空间。
vector必须是到
必要的时候才分配新的存储空间,
分配多少空间取决于其实现方式,不同的库采用不同的策略。每种实现都要遵循的原则:确保push_back操作高效地在vector中添加元素。
9.5 容器的选用
标准库的实现者已经最小化了内存分配的开销,元素是否连续分配会显著影响:
- 在容器的中间位置添加和删除元素的代价;
- 执行容器元素的随机访问的代价。
使用这些操作的程度将决定应该选择哪种容器。vector和deque提供了对元素的快速随机访问,
1. 插入操作如何影响容器选择
- list容器表示不连续的内存区域,允许向前和向后逐个遍历元素,在任何位置高效地insert和erase一个元素。list不支持随机访问。
- vector除了容器尾部,其他任何位置插入元素都要移动后面的元素。
- deque两端插入和删除元素非常快,中间插入和删除元素代价非常高。可以在首部和尾部高效地insert和erase,支持对所有元素的随机访问。
- 在deque的首部和尾部插入元素不会使迭代器失效,首部和尾部删除元素只会使指向删除元素的迭代器失效,在deque其他任何位置插入和删除操作都将使指向该容器的所有迭代器失效。
2. 元素的访问如何影响容器的选择
- vector和deque都支持高效的随机访问,list就慢很多。
- 通常来说,除非找到选择使用其他容器的更好的理由,否则选用vector是最佳选择。
3. 选择容器的提示
- 如果要随机访问,则应该选择vector或者deque;
- 如果要在容器中间位置插入或删除元素,则应该选择list;
- 如果程序不是在中间位置插入或删除,而是在首部或尾部插入和删除,则应该选用deque;
- 如果只需要读取的时候再中间插入,然后需要随机访问,可以考虑先用list读取,然后排序,接着讲list复制到vector中随机访问。
- 如果既要随机访问又要在中间插入和删除元素,此时就要综合考虑,看哪种操作更多来决定选择何种容器。
- 如果无法确定选用何种容器,代码中尽量用vector和list都提供的操作,使用迭代器,避免随机访问,这样必要时可以方便地将程序从vector改成list。
9.6 再谈string类型
to be added!
9.7 容器适配器
除了顺序容器,标准库还提供了三种顺序容器适配器:queue,priority_queue,stack。适配器是标准库中的通用概念,包括容器适配器、迭代器适配器、函数适配器。本质上,适配器是使一种事物的行为类似于另一种事物的行为的一种机制。容器适配器是让一种已存在的容器类型采用另一种不同的抽象类型的工作方式实现。例如,stack可以使任何一种顺序容器以栈的方式工作。
容器适配器通用的操作和类型
size_type |
一种类型,足以存储适配器类型最大对象的长度 |
value_type |
元素类型 |
container_type |
基础容器类型,适配器在此基础上实现 |
A a; |
创建一个新的空适配器a |
A a(c); |
创建一个新适配器初始化为容器c的副本。 |
关系操作符 |
所有适配器都支持全部关系操作符:==,!=,<,<=,>,>= |
1. 适配器初始化:所有的适配器都定义了上面两个构造函数初始化适配器。
2. 覆盖基础容器类型:默认的stack和queue是基于dequeue实现的,priority_queue是基于vector实现的。在创建适配器的时候可以通过将一个顺序容器指定为适配器的第二个类型实参,覆盖其关联的基础容器类型:
stack > str_stk;
stack > str_stk2(svec);
3. 适配器的关系运算:两个相同类型的适配器可以做相等、不相等、大于、小玉、大于等于、小于等于的
关系比较,
只要基础元素支持等于和小于操作符即可!
9.7.1 栈适配器
栈容器适配器支持的操作
s.empty() |
如果栈空返回true,否则false |
s.size() |
返回栈中元素个数 |
s.pop() |
删除站定元素,不返回其值 |
s.top() |
返回栈顶元素值,不删除 |
s.push(item) |
在栈顶压入新元素 |
所有的
容器适配器都根据其基础类型所支持的操作来定义自己的操作。默认情况下栈适配器建立在dequeue之上,push操作采用push_back实现。但是程序员
不能直接访问push_back操作。
9.7.2 队列和优先级队列
标准库提供了两种风格的队列:FIFO队列(queue),优先级队列(priority_queue),优先级队列不是将元素直接放在队尾,而是
放在比他低优先级的元素的前面,标准库
默认使用元素类型的 < 操作符来确定优先级关系。要使用这两种队列要包含queue头文件。
队列和优先级队列支持的操作
q.empty() |
返回true或false |
q.size() |
返回队列中元素的个数 |
q.pop() |
删除队首元素,但不返回其值 |
q.front() |
返回队首元素的值,但不删除。只用于queue |
q.back() |
返回队尾元素的值,但不删除,只用于queue |
q.top() |
返回具有最高优先级的元素值,但不删除,只用于priority_queue |
q.push(item) |
对于queue,在队尾压入新元素; 对于priority_queue,在基于优先级的适当位置插入新元素 |