9.1 顺序容器概述
vector:可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除数据很慢。
deque:双端队列。支持快速随机访问。在头尾插入删除速度很快。
list:双向链表。只支持双向顺序访问。在list中任何位置进行插入删除操作都很快。
forward_list:单向链表。只支持单向顺序访问。在链表的任意位置进行插入删除操作速度都很快。
array:固定大小数组。支持快速随机访问。不能添加或删除元素。
string:与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入删除速度快。
确定使用那种容器
Tip:
通常,使用vector是最好的选择,除非你有更好的理由选择其他容器。
一下是使用容器基本原则:
1.除非你有更好的理由选择其他容器,否则应使用vector。
2.如果你的程序有许多小的元素,且空间额外开销很重要,则不要使用list或forward_list。
3.如过程序要求随机访问元素,应使用vector或deque。
4.如果程序要求在容器的中间插入或删除元素,应使用list或forward_list。
5.如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用deque。
9.2.1 迭代器
一个迭代器范围由一对迭代器表示两个迭代器分别指向首元素和尾元素之后的位置。这两个迭代器通常被称为begin和end。
这种元素范围称为左闭合区间用数学描述为[begin,end)。表示范围字begin开始于end之前结束。
9.2.3 begin和end成员
begin和end操作生成指向容器中第一个元素和尾元素之后位置的迭代器。
begin和end有多个版本:带r的版本返回反向迭代器;以c开头的版本返回const迭代器。
不以c开头的版本都是重载版本。
以c开头的版本是C++新标准引入的,用以支持auto与begin和end函数结合使用。
注:
当不需要写访问时,应使用cbegin和cend。
9.2.4 容器定义和初始化
除array外其他容器的默认狗仔函数都会创建一个指定类型的空容器,且都可以接受指定容器大小和元素初始值的参数。
标准库array具有固定大小
当定义array时,除了指定元素类型,还要指定容器大小:
array<int, 4>//类型为:保存4个int数组
array<string, 10>//类型为:保存10个string的数组
array大小固定的特性也影响了他所定义的构造函数的行为。与其他容器不同,一个默认构造的array是非空的:它包含了与其大小一样多的元素。这些元素都被默认初始化。
如果我们对array进行列表初始化,初始值的数目必须等于或小于array的大小。
值得注意的是,虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制。
9.3 顺序容器的操作
9.3.1 向顺序容器 添加元素
使用push_back
关键概念:容器元素是拷贝
当我们使用一个对象来初始化容器时,或将一个对象插入到容器时,实际上放入容器中的是对象值的一个拷贝,而不是对象本身。就像我们将一个对象传递给非引用参数一样,容器中的对象与供值之间没有任何关联。
使用push_front
支持容器:list,forward_list,deque
不支持容器:vector
在容器中的特定位置添加元素
insert成员提供了更一般的添加功能,他允许我们在容器中任意位置插入0个或多个元素。vector、deque、list、和string都支持insert成员。forward_list提供了特殊版本的insert成员。
可以将元素插入到容器的开始为止,而不必担心容器是否支持push_front
注:
将元素插入到vector、deque、和string中的任何位置都是合法的。然而,这样做可能很耗时。
插入范围内元素
s.insert(s.end(), 10, "Anna")
这行代码将十个元素插入到s的末尾,将所有元素都初始化为Anna。
使用emplace操作
新标准引入了三个新成员——emplace_front、emplace和empla_back,这些操作构造而不是拷贝元素
当调用push或insert成员函数时,我们将元素类型的对象传递给他们,这些对象被拷贝到容器中。而当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。
9.3.3 删除元素
删除元素的成员函数并不检查其参数。在删除元素之前,程序员必须保证他们是存在的。
pop_front和pop_back成员函数
pop_front和pop_back成员函数分别删除首元素和尾元素。与vector和string不支持push_front一样,这些类型也不支持pop_front。类似的,forward_list不支持pop_back
从容器内部删除一个元素
成员函数erase从容器中指定位置删除元素。
删除多个元素
为了删除一个容器中的所有元素,我们既可以调用clear,也可以用begin和end获得迭代器作为参数调用erase:
slist.clear();
slist.erase(slist.begin(), slist.end());
9.3.4 特殊的forward_list操作
当添加或删除一个元素时,删除或添加的元素之前的那个元素的猴急会发生改变。
在一个forward_list中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。
forward_list并未定义insert、emplace和erase,而是定义了名为insert_after、emplace_after、erase_afer的操作。
当在forward_list中添加或删除元素时,我们必须关注两个迭代器——一个指向我们要处理的元素,另一个指向其前驱。
9.3.5 改变容器大小
我们可以用resize来增大或缩小容器,与往常一样,array不支持resize。
如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加扫容器后部。
9.3.6 容器操作可能使迭代器失效
向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。一个失效的指针、引用或迭代器将不再表示任何元素。使用失效的指针、引用或迭代器是一种严重的程序设计错误,可能会引起与使用未初始化指针一样的错误。
注:
由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确的重新定位迭代器。这个建议对vector、string和deque尤为重要。
编写改变容器的循环程序
添加/删除vector、string或deque 元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。
9.4 vector 对象是如何增长的
为了支持快速访问,vector将元素连续存储——每个元素紧挨着前一个元素存储。
标准库实现者采用了可以减少容器空间重新分配次数的策略,当不得不获取新的内存空间时,vector 和 string 的实现通常会分配比新的空间需求更大的内存空间。容器预留这些空间作为备用,可用来保存更多的新元素。
管理容量的成员函数
capacity 操作告诉我们容器在不扩张内存空间的情况下可以容纳多少个元素。reserve 操作允许我们通知容器他应该准备保存多少个元素。
注:
reserve 并不改变容器中元素的数量,它仅影响vector 预先分配多大色内存空间。
只有当需要的内存空间超过当前容量时,reserve 调用才会改变vector 的容量。如果需求大小大于当前容量,reverse 至少分配与需求一样大的内存空间。
每个vector实现都可以选择自己的内存分配策略。但必须遵守的一条原则是:只有当迫不得已时才可以分配新的空间。
capacity 和 size
只有在执行insert 操作时size 与 capacity 相等,或者调用 resize 或 reserve 时给定的大小超过当前 capacity 才可能重新分配内存空间。会分配多少超过给定容量的额外空间,取决于具体实现。
虽然不同的实现可以采用不同的分配策略,但所有的实现都应遵循一个原则:确保用push_back 向 vector 添加元素的操作有高效率。
9.5 额外的 string 操作
除了顺序容器的共同操作之外,string 类型还提供了一些额外的操作。
9.5.1 构造 string 的其他方法
除了与顺序容器相同的构造函数外,string 类型还支持另外的三个构造函数类型。
const char *cp="Hello World!!!"; //以空字符结束的数组
char noNull[]={'H','i'}; //不是以空字符结束
string sl(cp); //拷贝cp中的字符直到遇到空字符;sl =="Hello world!!!"
string s2(noNull,2); //从noNull拷贝两个字符;s2 =="Hi"
string s3(noNull); //未定义:noNull不是以空字符结束
string s4(cp +6,5); //从cp[6]开始拷贝5个字符;s4 =="World"
string s5(sl,6,5); //从s1[6]开始拷贝5个字符;s5 =="World"
string s6(s1,6); //从s1[6]开始拷贝,直至s1末尾;s6=="World!!!"
string s7(s1,6,20); //正确,只拷贝到s1末尾;s7 =="World!!!"
string s8(sl,16); //抛出一个out_of_range异常