6.5 标准库类型:顺序容器
概述
除我们介绍过的string和vector这两种外,C++标准库还包含其他的顺序容器,他们在下述两种方面存在不同的性能折中:
- 向容器添加或从容器中删除元素的代价
- 非顺序访问容器中元素的代价
顺序容器 | 介绍 |
---|---|
vector可变大小数组 | 支持快速随机访问,在尾部之外的位置插入或者删除元素可能很慢 |
deque双端队列 | 支持快速随机访问,在头尾位置插入/删除速度很快 |
list双向链表 | 只支持双向顺序访问,在list中任何位置进行插入/删除操作速度都很快 |
forward_list单向链表 | 只支持单向顺序访问,在链表任何位置进行插入/删除操作速度都很快 |
array固定大小数组 | 支持快速随机访问,不能添加或者删除元素 |
string字符串 | 和vector相似的容器,但专门用于保存字符,支持快速随机访问,在尾部插入/删除速度快 |
选择顺序容器的基本原则
- 除非你有很好的理由选择其他容器,否则优先用vector
- 如果你的程序有很多小元素,且空间的额外开销很重要,那么不要使用list或者
forward_list
- 如果程序要求随机访问元素,应使用vector或者deque
- 如果程序要求在容器的中间插入或删除元素,应使用list或者
forward_list
;如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用deque - 如果程序只有在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素
- 先判断是否真的需要在容器中间位置添加元素,如果仅仅是为了保证容器元素有序的话,可以在输入数据时直接向vector尾部追加数据,然后调用标准库中的sort函数重排容器中元素,从而避免在中间位置添加元素
- 如果必须在中间位置插入元素,那么可以考虑在输入阶段使用list,一旦输入完成,将list中的内容拷贝到一个vector中
容器库概览
以下列举所有容器(顺序容器和关联容器)都支持的操作。
1. 容器类型别名
通过类型别名,我们可以在不了解容器汇总元素类型的情况下使用它:
类型别名 | 含义 |
---|---|
iterator | 此容器类型的迭代器 |
const_iterator | 此容器类型的const迭代器 |
reverse_iterator | 此容器类型的反向迭代器 |
const_reverse_iterator | 此容器类型的const反向迭代器 |
size_type | 无符号整数类型,足够保存此种容器类型最大可能容器的大小 |
difference_type | 带符号整数类型,足够保存两个迭代器之间的距离 |
value_type | 元素类型 |
reference | 元素的左值类型,与value_type & 含义相同 |
const_reference | 元素的const左值类型,与const value_type& 含义相同 |
2. 容器的begin和end成员
begin和end操作生成指向容器中第一个元素和尾元素之后位置的迭代器,它们包含多个版本:
- 带
r
的版本返回反向迭代器 - 带
c
的版本返回const迭代器
3. 容器定义和初始化
容器定义和初始化的操作包括:
操作 | 含义 | 备注 |
---|---|---|
C c | 默认构造函数 | 如果C是一个array,则c中元素按默认方式初始化;否则c为空 |
C c1(c2) C c1 = c2 | c1初始化为c2的拷贝 | c1和c2必须是相同的容器类型,且保存相同的元素类型(对于array而言,两者还必须具有相同大小) |
C c{a, b, c...} C c = {a, b, c...} | c初始化为初始化列表中元素的拷贝 | 对于array类型列表中元素数目必须小于等于array的大小,任何遗漏的元素都进行值初始化 |
C c(b, e) | c初始化为迭代器b和e指定范围中元素的拷贝 | |
C seq(n) | seq包含n个元素,这些元素进行了值初始化 | 此构造函数是explicit的,array和string不适用 |
C seq(n, t) | seq包含n个初始化为值t的元素 | array不适用 |
4. 容器赋值和swap
Tips:assign操作不适用于关联容器和array
操作 | 含义 | 备注 |
---|---|---|
c1 = c2 | 将c1中的元素替换为c2中元素的拷贝 | c1和c2必须具有相同的类型 |
c = {a, b, c} | 将c1中元素替换为初始化列表中元素的拷贝 | array不适用 |
swap(c1, c2) c1.swap(c2) | 交换c1和c2中的元素 | c1和c2必须具有相同的类型,swap通常比从c2向c1拷贝元素快得多 |
seq.assign(b, e) | 将seq中的元素替换为迭代器b和e所表示的范围中的元素 | 迭代器b和e不能指向seq中的元素 |
seq.assign(il) | 将seq中的元素替换为初始化列表il中的元素 | |
seq.assign(n, t) | 将seq中的元素替换为n个值为t的元素 |
- 除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成
- 与其他容器不同,swap两个array会真正交换它们的元素,因此交换两个array所需的时间与array中元素的数目成正比
- 赋值相关操作会导致左边容器内部的迭代器、引用和指针失效,而swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效(容器类型为array和string的情况除外)
5. 容器大小操作
操作 | 含义 | 备注 |
---|---|---|
c.size() | c中元素的数目 | 不支持forward_list |
c.max_size() | c中可保存的最大元素数目 | |
c.empty() | 若c中存储了元素,返回false,否则返回true |
6. 容器的关系运算符
Tips:无序关联容器不支持关系运算符。
每个容器类型都支持相等运算符(==
和!=
),除了无序关联容器外所有容器都支持关系运算符(>
、>=
、<
和<=
)。关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。
Tips:比较两个容器实际上是进行元素的逐对比较:
- 如果两个容器具有相同大小且所有元素都两两对应相同,则这两个容器相等;否则两个容器不等
- 如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器
- 如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等元素的比较结果
顺序容器操作
1. 向顺序容器添加元素
Tips:除array外,所有标准库容器都提供灵活的内存管理,运行时可以动态添加或者删除元素来改变容器大小。
- array不支持添加元素
forward_list
有自己专有版本的insert
和emplae
,不支持push_back
和emplace_back
vector
和string
不支持push_front
和emmplace_front
操作 | 含义 | 备注 |
---|---|---|
c.push_back(t) c.emplace_back(args) | 在c的尾部创建一个值为t或者由args创建的元素,返回void | |
c.push_front(t) c.emplace_front(args) | 在c的头部创建一个值为t或者由args创建的元素,返回void | |
c.insert(p, t) c.emplace(p, args) | 在迭代器p指向的元素之前创建一个值为t或者由args创建的元素,返回指向新添加元素的迭代器 | |
c.insert(p, n, t) | 在迭代器p指向的元素之前插入n个值为t的元素,返回指向新添加的第一个元素的迭代器,如果n为0则返回p | |
c.insert(p, b, e) | 将迭代器b和e指定范围内的元素插入到迭代器p指向的元素之前,b和e不能指向c中的元素,返回指向新添加的第一个元素的迭代器,如果范围为空则返回p | |
c.insert(p, il) | il是一个花括号包围的元素值列表,将这些给定值插入到迭代器p指向的元素之前,返回指向新添加的第一个元素的迭代器,若列表为空则返回p |
Tips:向vector、string或者deque插入元素会使得所有指向容器的迭代器、引用和指针失效。
emplace_back()
是C++11
加入的新特性,都是用于将一个临时对象push
到容器的末尾。不同在于push_back()
需要先临时构造对象,再将这个对象拷贝到容器的末尾;而emplace_back()
直接在容器的末尾构造对象,省去了拷贝的过程。
push_back()
会先使用构造函数构造临时对象,然后需要调用拷贝构造函数将这个临时对象放入容器中,最后释放临时对象。这样造成的问题就是临时对象申请资源的浪费。emplact_back()
在容器尾部添加一个元素,不需要触发拷贝构造和转移构造
2. 访问元素
Tips:at和下标操作只适用于string、vector、deque和array;back不适用于
forward_list
需要注意:
- 对一个空容器调用front或者back,就像使用一个越界的下标一样,是一种严重的程序设计错误
- 在容器中访问元素的成员函数(front、back、下标和at)返回的都是引用,如果容器是一个const对象则返回const的引用
- 如果我们希望下包是合法的,可以使用at成员函数(at成员函数类似于下标运算符,但是在下标越界时会抛出一个
out_of_range
异常)
操作 | 含义 | 备注 |
---|---|---|
c.back() | 返回c中尾元素的引用 | 若c为空,则函数行为未定义 |
c.front() | 返回c中首元素的引用 | 若c为空,则函数行为未定义 |
c[n] | 返回c中下标为n的元素的引用 | n是一个无符号整数,若n>=c.size() 则函数行为未定义 |
c.at(n) | 返回下标为n的元素的引用 | 如果下标越界,则抛出out_of_range 异常 |
3. 删除元素
Tips:删除deque除首尾元素之外的任何元素都会使所有迭代器、引用和指针失效;指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。
需要注意:
- array不支持删除元素
forward_list
有特殊版本的eraseforward_list
不支持pop_back
,vector
和string
不支持pop_front
操作 | 含义 | 备注 |
---|---|---|
c.pop_back() | 删除c中尾元素,返回void | 若c为空,则函数行为未定义 |
c.pop_front | 删除c中首元素,返回void | 若c为空,则函数行为未定义 |
c.erase(p) | 删除迭代器p所指向的元素,返回一个指向被删元素之后元素的迭代器 | 若p是尾后迭代器,则函数行为未定义 |
c.erase(b, e) | 删除迭代器b和e所指定范围的元素,返回一个指向最后一个被删元素之后的元素的迭代器 | 若e本身就是尾后迭代器,则函数也返回尾后迭代器 |
c.clear() | 删除c中的所有元素,返回void |
4. 改变容器大小
Tips:如果resize缩小元素,那么指向被删除元素的迭代器、引用和指针都会失效;对vector、string或者deque进行resize可能导致迭代器、指针和引用失效。
操作 | 含义 | 备注 |
---|---|---|
c.resize(n) | 调整c的大小为n个元素,若n < c.size() 则多出的元素被丢弃,若必须添加新元素则对新元素进行值初始化 | 不适用于array |
c.resize(n, t) | 调整c的大小为n个元素,任何新添加的元素都初始化为值t | 不适用于array |
5. 容器操作可能使得迭代器失效
向容器中添加元素和从容器中删除元素的操作可能使得指向容器元素的指针、引用或者迭代器失效。
Tips:使用失效的指针、引用或者迭代器是一种严重的程序设计错误,很可能引用与使用未初始化指针一样的问题。
向容器添加元素后:
- vector和string:
- 存储空间被重新分配:指向容器的迭代器、指针和引用都会失效
- 存储空间未被重新分配:指向插入位置之前的元素的迭代器、指针和引用仍然有效,但指向插入位置之后元素的迭代器、指针和引用将会失效
- deque:
- 插入到除首尾位置之外的任何位置:迭代器、指针和引用都会失效
- 在首尾位置添加元素:迭代器会失效,但指向存在的元素的引用和指针不会失效
- list和
forward_list
:指向容器的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍然有效
当我们从容器删除一个元素后:
- list和
forward_list
:指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍然有效 - deque:
- 在首尾之外的任何位置删除元素:指向被删除元素外其他元素的迭代器、引用或指针也会失效
- 删除deque的尾元素:尾后迭代器失效,但其他迭代器、引用和指针不受影响
- 删除首元素:其他迭代器、引用和指针不受影响
- vector和string:指向被删元素之前元素的迭代器、引用和指针仍然有效
特殊的forward_list操作
从一个单向链表中添加或删除元素时,还会改变其前一个元素指向的下一个元素。由于这些操作与其他容器上的操作实现方式不同,因此forward_list
并未定义insert
、emplace
和erase
,而是定义了名为insert_after
、emplace_after
和erase_after
的操作。> Tips:为了支持这些操作,forward_list
定义了before_begin
首前迭代器。
操作 | 含义 | 备注 |
---|---|---|
lst.before_begin() lst.cbefore_begin() | 返回指向链表首元素之前不存在的元素的迭代器,此迭代器不能解引用,cbefore_begin() 返回一个const_iterator | |
lst.insert_after(p, t) lst.insert_after(p, n, t) lst.insert_after(p, b, e) lst.insert_after(p, il) | 在迭代器p之后的位置插入元素,其中t是一个对象,n是数量,b和e是表示范围的一对迭代器(b和e不能指向lst内),il是一个花括号列表 | 返回一个指向最后一个插入元素的迭代器,如果范围为空则返回p,若p为尾后迭代器则函数行为未定义 |
emplace_after(p, args) | 使用args在p指定的位置之后创建一个元素 | 返回一个指向这个新元素的迭代器,若p为尾后迭代器则函数行为未定义 |
lst.erase_after(p) lst.erase_after(b, e) | 删除p指向的位置之后的元素,或删除从b之后直到e之间的元素 | 返回一个指向被删元素之后的迭代器,若不存在这样的元素则返回尾后迭代器,如果p指向lst的尾元素或者一个尾后迭代器则函数行为未定义 |