第九章 顺序容器
一、顺序容器概述
- 顺序容器(sequential container):为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。
- 不同容器在不同的方面都有不同的性能折中:
- 向容器添加或者删除元素的代价
- 非顺序访问容器中元素的代价
1. 顺序容器类型
容器类型 |
解释 |
vector |
可变大小数组。支持快速随机访问。在尾部之外的位置进行插入/删除元素操作可能很慢。 |
deque |
双端队列。支持快速随机访问。在头尾位置进行插入/删除操作速度很快。 |
list |
双向链表。只支持双向顺序访问。在任何位置进行插入/删除操作速度很快。 |
forward_list |
单向链表。只支持单向顺序访问。在任何位置进行插入/删除操作速度很快。 |
array |
固定大小数组。支持快速随机访问,不能进行添加/删除元素操作。 |
string |
专门用来保存字符,与vector相似的容器。支持快速随机访问。在尾部进行插入/删除元素操作很块。 |
- 除了固定大小的array外,其他容器都提供高效、灵活的内存管理。
-
- forward_list和array是新C++标准增加的类型。
- 通常使用vector是最好的选择,除非你有很好的理由选择其他容器。
- 新标准库的容器比旧版的快得多
二、容器库概览
1. 容器操作
类型别名
操作 |
解释 |
iterator |
此容器的迭代器类型 |
const_iterator |
可以读取元素,但不能修改元素的迭代器类型 |
size_type |
无符号整数类型,足够保存此种容器类型最大可能容器的大小 |
difference_type |
带符号整数类型,足够保存两个迭代器之间的距离 |
value_type |
元素类型 |
reference |
元素的左值类型;和value_type &含义相同 |
const_reference |
元素的const左值类型,即const value_type & |
构造函数
操作 |
解释 |
C c; |
默认构造函数,构造空容器 |
C c1(c2);或C c1 = c2; |
构造c2的拷贝c1 |
C c(b, e) |
构造c,将迭代器b和e指定范围内的所有元素拷贝到c(array不支持) |
C c{a, b, c...} |
列表初始化c |
C c(n) |
只支持顺序容器,且不包括array,包含n个元素,这些元素进行了值初始化 |
C c(n, t) |
包含n个初始值为t的元素 |
- 只有顺序容器的构造函数才接受大小参数,关联容器并不支持。
- array具有固定大小。
- 和其他容器不同,默认构造的array是非空的。
- 直接复制:将一个容器复制给另一个容器时,类型必须匹配:容器类型和元素类型都必须相同。
- 使用迭代器复制:不要求容器类型相同,容器内的元素类型也可以不同。
赋值和swap
操作 |
解释 |
c1 = c2; |
将c1中的元素替换成c2中的元素 |
c1 = {a, b, c...} |
将c1中的元素替换成列表中的元素(不适用于array) |
c1.swap(c2) |
交换c1和c2的元素 |
swap(c1, c2) |
等价于c1.swap(c2) |
c.assign(b, e) |
将c中的元素替换成迭代器b和e表示范围中的元素,b和e不能指向c中的元素 |
c.assign(il) |
将c中的元素替换成初始化列表il中的元素 |
c.assign(n, r) |
将c中的元素替换为n个值是t的元素 |
- 使用非成员版本的swap是一个好习惯。
- assign操作不适用于关联容器和array,仅适用于顺序容器。
- 赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效(array和string除外)。
大小
操作 |
解释 |
c.size() |
c中元素的数目(不支持forward_list) |
c.max_size() |
c中可保存的最大元素数目 |
c.empty() |
若c中存储了元素,返回false,否则返回true |
添加元素
操作 |
解释 |
c.push_back(t) |
在c尾部创建一个值为t的元素,返回void |
c.emplace_back(args) |
同上 |
c.push_front(t) |
在c头部创建一个值为t的元素,返回void |
c.emplace_front(args) |
同上 |
c.insert(p, t) |
在迭代器p指向的元素之前创建一个值是t的元素,返回指向新元素的迭代器 |
c.emplace(p, args) |
同上 |
c.inset(p, n, t) |
在迭代器p指向的元素之前插入n个值为t的元素,返回指向第一个新元素的迭代器;如果n是0,则返回p |
c.insert(p, b, e) |
将迭代器b和e范围内的元素,插入到p指向的元素之前;如果范围为空,则返回p |
c.insert(p, il) |
il是一个花括号包围中的元素值列表,将其插入到p指向的元素之前;如果il是空,则返回p |
- 因为这些操作会改变大小,因此不适合于array。
- forward_list有自己专有版本的insert和emplace。
- forward_list不支持push_back和emplace_back。
- 当我们用一个对象去初始化容器或者将对象插入到容器时,实际上放入的是对象的拷贝。
- emplace开头的函数是新标准引入的,这些操作是构造而不是拷贝元素。
- 传递给emplace的参数必须和元素类型的构造函数相匹配。
- 向一个vector、string或deque插入元素会使所有指向内容的迭代器、引用和指针失效。
删除元素
操作 |
解释 |
c.pop_back() |
删除c中尾元素,若c为空,则函数行为未定义。函数返回void |
c.pop_front() |
删除c中首元素,若c为空,则函数行为未定义。函数返回void |
c.erase(p) |
删除迭代器p指向的元素,返回一个指向被删除元素之后的元素的迭代器,若p本身是尾后迭代器,则函数行为未定义 |
c.erase(b, e) |
删除迭代器b和e范围内的元素,返回指向最后一个被删元素之后元素的迭代器,若e本身就是尾后迭代器,则返回尾后迭代器 |
c.clear() |
删除c中所有元素,返回void |
- 会改变容器大小,不适用于array。
- forward_list有特殊版本的erase
- forward_list不支持pop_back
- vector和string不支持pop_front
- 删除deque中除收尾位置之外的任何元素都会使所有迭代器、引用和指针都会失效。指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。
访问元素
操作 |
解释 |
c.back() |
返回c中尾元素的引用。若c为空,函数行为未定义 |
c.front() |
返回c中头元素的引用。若c为空,函数行为未定义 |
c[n] |
返回c中下标是n的元素的引用,n时候一个无符号整数。若n>=c.size(),则函数行为未定义。 |
c.at(n) |
返回下标为n的元素引用。如果下标越界,则抛出out_of_range异常 |
- 访问成员函数返回的是引用。
- at和下标操作只适用于string、vector、deque、array。
- back不适用于forward_list。
- 如果希望下标是合法的,可以使用at函数。
特殊的forward_list操作
操作 |
解释 |
lst.before_begin() |
返回指向链表首元素之前不存在的元素的迭代器,此迭代器不能解引用。 |
lst.cbefore_begin() |
同上,但是返回的是常量迭代器。 |
lst.insert_after(p, t) |
在迭代器p之后插入元素。t是一个对象 |
lst.insert_after(p, n, t) |
在迭代器p之后插入元素。t是一个对象,n是数量。若n是0则函数行为未定义 |
lst.insert_after(p, b, e) |
在迭代器p之后插入元素。由迭代器b和e指定范围。 |
lst.insert_after(p, il) |
在迭代器p之后插入元素。由il指定初始化列表。 |
emplace_after(p, args) |
使用args在p之后的位置,创建一个元素,返回一个指向这个新元素的迭代器。若p为尾后迭代器,则函数行为未定义。 |
lst.erase_after(p) |
删除p指向位置之后的元素,返回一个指向被删元素之后的元素的迭代器,若p指向lst的尾元素或者是一个尾后迭代器,则函数行为未定义。 |
lst.erase_after(b, e) |
类似上面,删除对象换成从b到e指定的范围。 |
- 链表在删除元素时需要修改前置节点的内容,双向链表会前驱的指针,但是单向链表没有保存,因此需要增加获取前置节点的方法。
- forward_list定义了before_begin,即首前(off-the-begining)迭代器,允许我们再在首元素之前添加或删除元素。
改变容器大小
操作 |
解释 |
c.resize(n) |
调整c的大小为n个元素,若n<c.size(),则多出的元素被丢弃。若必须添加新元素,对新元素进行值初始化。 |
c.resize(n, t) |
调整c的大小为n个元素,任何新添加的元素都初始化为值t。 |
- 如果resize缩小容器,则指向被删除元素的迭代器、引用和指针都会失效;对vector、string或deque进行resize可能导致迭代器、引用和指针失效。
获取迭代器
操作 |
解释 |
c.begin(), c.end() |
返回指向c的首元素和尾元素之后位置的迭代器 |
c.cbegin(), c.cend() |
返回const_iterator |
- 以c开头的版本是C++11新标准引入的
- 当不需要写访问时,应该使用cbegin和cend。
反向容器的额外成员
操作 |
解释 |
reverse_iterator |
按逆序寻址元素的迭代器 |
const_reverse_iterator |
不能修改元素的逆序迭代器 |
c.rbegin(), c.rend() |
返回指向c的尾元素和首元素之前位置的迭代器 |
c.crbegin(), c.crend() |
返回const_reverse_iterator |
2. 迭代器
- 迭代器范围:begin到end,即第一个元素到最后一个元素的后面一个位置。
- 左闭合区间:[begin, end)。
- 使用左闭合蕴含的编程设定:
- 如果begin和end相等,则范围为空。
- 如果二者不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素。
- 可以对begin递增若干次,使得begin == end。
三、顺序容器操作
1. 容器操作可能使迭代器失效的情况
- 在向容器添加元素后:
- 对于vector和string,如果存储空间被重新分配,则指向容器的迭代器、引用和指针都会失效。如果存储空间未被重新分配,则指向插入位置之前的元素的迭代器、引用和指针仍然有效,但是之后的都会无效,
- 对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、引用和指针失效。如果在首尾位置添加元素,那么迭代器会失效,但是指向存在的元素的引用和指针不会失效。
- 对于list和forward_list,指向容器的迭代器、引用和指针仍然有效。
- 在从容器删除元素后:
- 对于vector和string,指向被删元素之前元素的迭代器、引用和指针仍然有效。
- 对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用和指针都会失效。如果在尾位置删除元素,则尾后迭代器失效,但是其他迭代器、引用和指针仍然有效。如果在首位置删除元素,其他迭代器、引用和指针仍然有效。
- 对于list和forward_list,指向容器其他位置的迭代器、引用和指针仍然有效。
- 注意:当我们删除元素时,尾后迭代器总会失效。使用失效的迭代器、指针、引用是严重的运行时错误!
- 建议:将要求迭代器必须保持有效的程序片段最小化。并且不要保存end返回的迭代器。
2. 容器内元素的类型约束
- 元素类型必须支持赋值运算;
- 元素类型的对象必须可以复制。
- 除了输入输出标准库类型外,其他所有标准库类型都是有效的容器元素类型。
四、vector对象是如何增长的
vector和string在内存中是连续保存的,如果原先分配的内存位置已经使用完,则需要重新分配新空间,将已有元素从就位置移动到新空间中,然后添加新元素。但是如果这样,性能会很差。
为了避免这种代价,标准库采用了可以减少容器空间重新分配次数的策略。即,当不得不获取新的内存空间时,vector和string的实现通常会分配比新的空间需求更大的内存空间。
管理容器的成员函数
操作 |
解释 |
c.shrink_to_fit |
将capacity()减少到和size()相同大小 |
c.capacity() |
不重新分配内存空间的话,c可以保存多少个元素 |
c.reserve(n) |
分配至少能容纳n个元素的内存空间 |
- 注意:reserve并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间。
五、额外的string操作
1. 构造string的其他方法
操作 |
解释 |
string s(cp, n) |
s是cp指向的数组中前n个字符的拷贝,此数组至少应该包含n个字符。 |
string s(s2, pos2) |
s是string s2从下标pos2开始的字符的拷贝。若pos2 > s2.size(),则构造函数的行为未定义。 |
string s(s2, pos2, len2) |
s是string s2从下标pos2开始的len2个字符的拷贝。 |
2. substr操作
操作 |
解释 |
s.substr(pos, n) |
返回一个string,包含s中从pos开始的n个字符的拷贝。pos的默认值为0。n的默认值为s.size()-pos,即拷贝从pos开始的所有字符。 |
3. 改变string其他方法
操作 |
解释 |
s.insert(pos, args) |
在pos之前插入args指定的字符。pos可以使是下标或者迭代器。接受下标的版本返回指向s的引用;接受迭代器的版本返回指向第一个插入字符的迭代器。 |
s.erase(pos, len) |
删除从pos开始的len个字符,如果len被省略,则删除后面所有字符,返回指向s的引用。 |
s.assign(args) |
将s中的字符替换成args指定的字符。返回一个指向s的引用。 |
s.append(args) |
将args指定的字符追加到s,返回一个指向s的引用。 |
s.replace(range, args) |
删除s中范围range中的字符,替换成args指定的字符。返回一个指向s的引用。 |
4. string搜索操作
- string类提供了6个不同的搜索函数,每个函数都有4个重载版本。
- 每个搜索操作都返回一个string::size_type值,表示匹配发生位置的下标。如果搜索失败则返回一个名为string::npos的static成员(类型是string::size_type,初始化值是-1,也就是string最大的可能大小)
操作 |
解释 |
s.find(args) |
查找s中args第一次出现的位置 |
s.rfind(args) |
查找s中args最后一次出现的位置 |
s.find_first_of(args) |
在s中查找args中任何一个字符第一次出现的位置 |
s.find_last_of(args) |
在s中查找args中任何一个字符最后一次出现的位置 |
s.find_first_not_of(args) |
在s中查找第一个不在args中的字符 |
s.find_first_not_of(args) |
在s中查找最后一个不在args中的字符 |
args必须是一下的形式之一:
args形式 |
解释 |
c, pos |
从s中位置pos开始查找字符c。pos默认是0 |
s, pos |
从s中位置pos开始查找字符串s。pos默认是0 |
cp, pos |
从s中位置pos开始查找指针cp指向的以空字符结尾的C风格字符串。pos默认是0 |
cp, pos, n |
从s中位置pos开始查找指针cp指向的前n个字符。pos和n无默认值。 |
5. compare函数
逻辑类似于C标准库的strcmp函数,根据s是等于、大于还是小于参数指定的字符串,s.compare返回0、正数或负数。
参数形式 |
解释 |
s2 |
比较s和s2 |
pos1, n1, s2 |
比较s从pos1开始的n1个字符和s2 |
pos1, n1, s2, pos2, n2 |
比较s从pos1开始的n1个字符和s2 |
cp |
比较s和cp指向的以空字符结尾的字符数组 |
pos1, n1, cp |
比较s从pos1开始的n1个字符和cp指向的以空字符结尾的字符数组 |
pos1, n1, cp, n2 |
比较s从pos1开始的n1个字符和cp指向的地址开始n2个字符 |
6. 数值转换
操作 |
解释 |
to_string(val) |
一组重载函数,返回数值val的string表示。val可以使任何算术类型。对每个浮点类型和int或更大的整型,都有相应版本的to_string()。和往常一样,小整型会被提升。 |
stoi(s, p, b) |
返回s起始子串(表示整数内容)的数值,p是s中第一个非数值字符的下标,默认是0,b是转换所用的基数。返回int |
stol(s, p, b) |
返回long |
stoul(s, p, b) |
返回unsigned long |
stoll(s, p, b) |
返回long long |
stoull(s, p, b) |
返回unsigned long long |
stof(s, p) |
返回s起始子串(表示浮点数内容)的数值,p是s中第一个非数值字符的下标,默认是0。返回float |
stod(s, p) |
返回double |
stold(s, p) |
返回long double |
六、容器适配器
- 除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue、priority_queue。
- 适配器(adapter)是使一事物的行为类似于另一事物的行为的一种机制,例如 stack 可以使任何一种顺序容器(除array或forward_list)以栈的方式工作。
- 默认情况下,stack和queue是基于deque实现的,priority_queue是基于vector实现的。
- 注意:所有适配器都要求容器具有添加和删除元素以及访问尾元素的能力。
// 每个适配器都定义两种构造函数:1.默认构造函数创建一个空对象。2. 接受一个容器的构造函数拷贝该容器来初始化适配器。
// 1.
deque<int> deq;
stack<int> stk(deq);
// 2.
stack<string, vector<string> > str_stk;
1. 适配器的通用操作和类型
操作 |
解释 |
size_type |
一种类型,须以保存当前类型的最大对象的大小 |
value_type |
元素类型 |
container_type |
实现适配器的底层容器类型 |
A a; |
创建一个名为a的空适配器 |
A a(c) |
创建一个名为a的适配器,带有容器c的一个拷贝 |
关系运算符 |
每个适配器都支持所有关系运算符:==、!=、<、 <=、>、>=这些运算符返回底层容器的比较结果 |
a.empty() |
若a包含任何元素,返回false;否则返回true |
a.size() |
返回a中的元素数目 |
swap(a, b) |
交换a和b的内容,a和b必须有相同类型,包括底层容器类型也必须相同 |
a.swap(b) |
同上 |
2. stack
操作 |
解释 |
s.pop() |
删除栈顶元素,不返回。 |
s.top() |
返回栈顶元素,不删除。 |
s.push(item) |
创建一个新元素,压入栈顶,该元素通过拷贝或移动item而来。 |
s.emplace(args) |
同上,但元素由args来构造。 |
- 定义在stack头文件中。
- stack默认基于deque实现,也可以在list或vector之上实现。
3. queue和priority_queue
操作 |
解释 |
q.pop() |
删除队首元素,但不返回。 |
q.front() |
返回队首元素的值,不删除。 |
q.back() |
返回队尾元素的值,不删除。只适用于queue |
q.top() |
返回具有最高优先级的元素值,不删除。 |
q.push(item) |
在队尾压入一个新元素。 |
q.emplace(args) |
同上,但元素由args来构造。 |
- 定义在queue头文件中。
- queue默认基于deque实现,priority_queue默认基于vector实现。
- queue可以在list或vector之上实现,priority_queue也可以用deque实现。