C++ STL学习
目录容器库概览
对可以保存在容器中的元素的限制
容器需要支持拷贝操作,因此,保存在容器中的元素要能够支持拷贝操作。可以为不支持特定操作需求的元素类型定义容器,但是这样就只能使用容器那些不要求容器元素有特定操作的操作类型。
一般来说,每种容器都定义在一个与容器名相同的头文件中,对大多数容器,但不是所有容器,需要提供元素类型来生成特定的容器。
特别的,容器的元素的类型可以是另一种容器,只要在外层容器的<>
中指定内层容器的类型即可。在老版本的编译器中,这种以其他容器为元素的容器,在声明或者定义时,需要在内层容器的<>
和外层容器的<>
之间插入空格,如vector<vector<int> > v1
,新版本的编译器则无此要求。
容器支持的操作
容器类型支持的操作可以根据容器的大类分成一定的层次:
- 顺序容器和关联容器都支持的操作;
- 仅部分顺序容器支持的操作;
- 仅部分关联容器支持的操作;
- 少数特殊的容器,如
array
、forward_list
或者无序容器支持的操作。
所有容器都支持的操作或容器成员
下表是所有容器都支持的操作。
支持操作或成员 | 说明 |
---|---|
类型别名 | |
iterator |
此容器类型的迭代器 |
const_iterator |
const 类型的iterator |
difference_type |
有符号整型,足够表示此种容器的两个迭代器之间的距离 |
value_type |
此容器的元素的类型 |
size_type |
无符号整型,足够表示此种容器可能的最大容器大小 |
reference |
元素的左值类型,等价于value_type & |
const_reference |
const 版本的reference ,等价于const value_type & |
构造函数 | |
C c |
默认构造函数,构造空容器 |
C c1(c2) |
使用c2 拷贝构造c1 ,c2 和c1 的容器类型和元素类型必须一样 |
C c1 = c2 |
使用c2 拷贝构造c1 ,c2 和c1 的容器类型和元素类型必须一样 |
C c(b, e) |
使用迭代器b 和e 的半开区间[b,e) 拷贝构造c ,不要求容器类型和元素类型一样,只要求[b, e) 中的元素和c 的元素类型相容 |
C c{a, d, ...} |
列表初始化c |
C c = {a, b, ...} |
将c 初始化为列表中元素的拷贝 |
赋值和swap |
|
c1 = c2 |
将c1 种的元素替换为c2 中的元素 |
c1 = {a, b, ...} |
将c1 中的元素替换为列表中的元素(《C++ Primer》中说array 不支持此操作,其实使用g++ 测试发现array 是支持此操作的) |
c1.swap(c2) |
交换c1 和c2 中的元素 |
swap(c1, c2) |
等价于c1.swap(c2) |
添加、删除元素 | |
(array 不支持,在不同的容器中,接口可能不同) |
|
c.insert(args) |
将args 中的元素拷贝进c |
c.empalce(inits) |
使用inits 在c 中原地构造一个c 的元素 |
c.erease(args) |
将args 指定的元素从c 中删除 |
c.clear() |
删除c 中的所有元素,返回void |
比较操作 | |
==, != |
相等(不相等)的关系运算符 |
<, <=, >=, > (无序关联容器不支持) |
小于、不大于、不小于、大于的关系运算符 |
获取迭代器 | |
c.begin(), c.end() |
指向c 的首元素和尾元素后一位置的迭代器 |
c.cbegin(), c.cend() |
指向c 的首元素和尾元素后一位置的const 类型的迭代器 |
反向容器的额外成员 | |
(forward_list 不支持) |
|
reverse_iterator |
按逆序寻址元素的迭代器 |
const_reverse_iterator |
const 版本的reverse_ietator |
c.rbegin()、c.rend() |
指向c 的首元素之前位置和尾元素的迭代器 |
c.crbegin()、c.crend() |
const 版本的c.rbegin()、c.rend() |
迭代器
称某种数据类型是迭代器,当且仅当此类型支持一套操作,此套操作可以访问元素或者从某个元素移动到另一个元素。
迭代器有着相同的接口:如果某个迭代器可以提供某种操作,那么所有提供相同操作的迭代器在实现上都是相同的。
迭代器的公共操作
下表是所有的迭代支持都的操作。
操作 | 说明 |
---|---|
iter1 == iter2 |
当iter1 和iter2 指向同一个容器中的同一个元素或尾后迭代器时成立 |
iter1 != iter2 |
等价于!(iter1 == iter2) |
迭代器的类型
迭代器的const
属性
所有的拥有迭代器的标准库类型使用两个关键字来表示迭代器类型:
iterator
表示通过此类迭代器可以对迭代器指向的元素进行读写;const_iterator
表示通过此类型只能迭代器指向的元素进行读操作,不可改变迭代器指向的元素的值。
对于const
类型的容器,只能使用const_iterator
的迭代器。
迭代器的操作类型
可以按照迭代器的操作类型将迭代器分成5类,这5类迭代器形成一种操作层次:除输出迭代器外(输出迭代器可以看做输入迭代器在功能上的补集——输出迭代器只写而不读元素),高层次的迭代器支持低层次的迭器所支持的所有操作。它们所支持的操作范围可以用下列的不等式表示:
随机访问迭代器 > 双向迭代器 > 前向迭代器 > (输入迭代器(读元素),输出迭代器(写元素))
读写元素 = 输入迭代器(读元素)+ 输出迭代器(写元素)
迭代器类型 | 支持的操作 |
---|---|
输入迭代器 | 除支持的公共操作外,支持前置的和后置的++ ,它使输入迭代器指向容器中的下一个元素位置。支持* 解引用运算符,它只会出现在= 右侧,用于读取元素。支持-> 箭头运算符,等价于(*it).mem ,它解引用迭代器,并提取被指向对象的成员。输入迭代器只能顺序读取容器中的元素,递增迭代器可能使所有指向流的迭代器失效 |
输出迭代器 | 除支持的公共操作外,支持前置的和后置的++ ,它使输入迭代器指向容器中的下一个元素位置。支持* 解引用运算符,它只会出现在= 左侧,用于写元素。只能向一个输出迭代器赋值一次 |
前向迭代器 | 支持输入迭代器和输出迭代器的所有操作。只能在序列中沿着一个方向移动。可以多次读写同一个元素 |
双向迭代器 | 除支持前向迭代器的所有操作外,还支持前置的和后置的-- 运算符。可前向、后向移动,读写序列中的元素 |
随机访问迭代器 | 除支持双向迭代器的所有操作,还支持迭代器的算术运算。提供在常量时间内访问序列中任意元素的能力 |
随机访问迭代器支持的算术运算如下表所示。
算术运算类型 | 含义 |
---|---|
iter + n |
迭代器加上一个整型数,结果仍然是一个迭代器,指示的位置比iter 指示的位置前进了n 个单位,结果或是指向一个元素,或者一个尾后迭代器 |
iter - n |
迭代器减去一个整型数,结果仍然是一个迭代器,指示的位置比iter 指示的位置后退了n 个单位,结果或是指向一个元素,或者一个尾后迭代器 |
iter += n |
迭代器加法的复合赋值语句,将iter 的值加上n 后再赋值给iter |
iter -= n |
迭代器减法的复合赋值语句,将iter 的值减去n 后再赋值给iter |
iter1 - iter2 |
迭代器相减的结果是它们之间的距离,表示ite2r 向前移动差值个单位后将得到iter1 。参与运算的两个迭代器必须是指向同一个容器的迭代器,或是同一个容器的尾后迭代器 |
>, >=, <, <= |
迭代器的关系运算符,若迭代器指向的位置在另一个迭代器指向的位置之前,则说前者小于后者。参与关系运算的两个迭代器必须是指向同一个容器的迭代器,或是同一个容器的尾后迭代器 |
迭代器范围
一个迭代器范围由一对迭代器表示,这个两个迭代器分别指向同一个容器中的元素,或是同一个容器中的尾后位置。这两个迭代器分别用begin
和end
表示(或是用first
和last
表示),他们标记了一个容器中的一个元素范围。
通常end
或last
不会指向容器中的一个元素,而是指向容器中的尾后位置,这样的元素范围被称为左闭合区间,其数学描述为:[begin,end)
,表示范围自begin
起,至end
前结束,begin
和end
可以指向相同的位置,但是不能指向容器首元素之前的位置。
使用左闭合区间的编程假定
标准库使用左闭合区间表示元素范围是因为左闭合区间有下面三种方便的性质,假定begin
和end
表示一个合法的迭代器范围,则有:
- 范围为空当且仅当
begin == end
; - 若
begin != end
,则范围内至少存在一个元素; - 可以通过递增
begin
使begin == end
。
顺序容器
顺序容器的特点是元素在容器中的相对位置和元素加入容器时的先后顺序有关,元素之间的逻辑结构是线型的。
关联容器的特点是元素在容器中的相对位置和与每个元素绑定的关键字有关,元素之间的逻辑结构是非线型的,如树型结构、表型结构或者其他非线型结构。
顺序容器概述
顺序容器的类型和特点
C++标准库提供了如下的顺序容器。
容器类型 | 特点 |
---|---|
vector |
动态数组,逻辑上相邻的元素,在内存中也相邻。支持随机访问元素,不能在直接头部删除和添加元素(借助迭代器指向待删除的元素可以)。在一次添加、删除元素的操作后,在添加、删除位置之后的所有元素在内存中都要移动位置来保持元素连续存储的特点,在一次添加、删除元素后可能容器的容量会改变,所以元素要整体移动到内存中的新位置去,所以适合在尾部添加元素,在其他位置添加元素效率很低 |
string |
和vector 类似的容器,和vector 之间的区别是string 容器元素是字符 |
decque |
双端队列,支持随机访问。在底层实现上,采用链表和动态数组的方式。在头部和尾部添加、删除元素效率高,在其他位置添加、删除元素效率低 。 |
list |
双向链表,不支持随机访问,在逻辑上相邻的元素,在内存中不一定相邻。在任何位置添加、删除元素效率高 |
forward_list |
单向链表,只支持从头部向尾部的单向遍历,不支持随机访问,在逻辑上相邻的元素,在内存中不一定相邻。在任何位置添加、删除元素的效率高。 |
array |
固定大小的数组,支持随机访问,不能添加、删除元素 |
顺序容器在以下两个方面有不同程度的性能折中:
- 向容器中添加和删除元素的开销;
- 非顺序访问元素的开销。
确定使用哪种顺序容器
如无必要,使用vector
容器,除非你有充分的理由选择其他容器。
在以下情况时可以考虑使用别的容器:
- 如果程序要求在容器中间添加、删除元素,使用
list
或者更forward_list
; - 如果要求程序在头尾添加、删除元素,但是不会在中间添加、删除元素,使用
deque
。
顺序容器的操作
顺序容器的定义和初始化
除所有容器都支持的容器的定义和初始化操作外,仅有顺序容器(除array
外)还支持接受一个大小参数的构造函数,如下表所示。
构造函数 | 含义 |
---|---|
C seq(n) |
seq 包含了n 个元素,这些元素进行了值初始化,此构造函数是explict 属性的 |
C seq(n, t) |
seq 包含了n 个元素,这些元素通过t 进行初始化 |
向顺序容器添加元素
下表是
操作 | 含义 |
---|---|
向容器中添加元素的操作会改变容器的大小,array 类型具有固定的大小,因此array 不支持添加元素的操作。 |
|
vector 和strng 不支持push_front 、emplace_front 操作。forward_list 有自己版本的insert 和emplace 操作,不支持push_back 、emplace_back 、emplace_front 、push_front 操作 |
|
c.push_back(t) |
使用t 在c 的尾部拷贝构造一个元素 |
c.empalce_back(inits) |
使用inits 在c 的尾部就地构造一个元素 |
c.push_front(t) |
使用t 在c 的首元素拷贝构造一个元素,容器中原有的所以元素都要后移一个位置 |
c.emplace_front(inits) |
使用inits 在c 的头部就地构造一个元素,容器中原有的所以元素都要后移一个位置 |
c.insert(p, t) ,c.insert(p, inits) |
在迭代器p 指向的位置之前创建一个值为t 或是由inits 拷贝初始化的元素,返回一个指向被插入的元素的迭代器 |
c.insert(p, b, e) |
将迭代器范围[b, e) 内的元素插入到迭代器p 指向的位置前,返回一个指向最新被插入元素的迭代器 |
c.insert(p, n , t) |
在迭代器p 指向的位置前插入n 个值为t 的元素,返回一个指向最新被插入元素的迭代器 |
c.insert(p, il) |
在迭代器p 指向的位置前插入一个由{} 包围的元素值列表中的元素,返回一个指向最新被插入的元素的迭代器 |
初始化和插入操作的关键概念
- 被插入容器中的元素是传递给容器的元素的拷贝,而不是传递给容器的元素本身,在初始化操作或者插入操作完成后,容器中的元素对象和提供值的元素之间就没有关系了,对容器中的元素对象的操作不会对提供值的元素对象产生影响,反之亦然。
- 在插入操作中,
insert
、push
操作和emplace
操作由效率上的区别。如果传递给容器的参数是容器元素的一个左值,则insert
和push
操作会调用容器元素的拷贝构造函数,将传给容器的对象拷贝给容器元素的对象来完成插入操作;如果传递给容器的参数是容器元素的一个右值,则可能会调用容器元素对象的移动构造函数(具体情况视编译器的实现而定),而emplace
操作会直接调用与传入容器的对象相匹配的构造函数来在容器的内存空间中就地构造元素对象,没有了拷贝操作。 - 在使用
insert
进行插入的操作中,迭代器可以是反向迭代器,这样最新被插入的元素是序列中的第一个元素。
访问操作
下表定义了顺序容器对容器中成员的访问操作
操作类型 | 含义 |
---|---|
forward_list 不支持back 操作;下标和at 操作只适用于vector、string、deque、array |
|
c.back() |
返回c 中尾元素的引用,若c 为空,则行为未定义 |
c.front |
返回c 中首元素的引用,若c 为空,则行为未定义 |
c[n] |
返回c 中下标为n 的元素的引用,要求n >= 0 && n <= c.size() ,否则行为未定义 |
c.at(n) |
返回c 中下标为n 的元素的引用,如果下标越界,则抛出out_of_range 的异常 |
删除元素的操作
下表定义了顺序容器删除元素的操作
操作 | 含义 |
---|---|
删除操作会改变容器的大小,所有array 不适用删除操作。forward_list 有自己版本的erease ,不支持pop_back 。vector、string 不支持pop_front |
|
c.pop_back() |
删除c 中的首元素,若c 为空,则行为未定义。函数返回void |
c.pop_back() |
删除c 的尾元素,若c 为空,则行为未定义。函数返回void |
c.erease(p) |
删除迭代器p 指向的元素,返回一个指向被删除元素下一位置的迭代器,若p 指向尾元素,则返回尾后迭代器。若p 是尾后迭代器,则函数行为未定义 |
c.erease(b, e) |
删除迭代器范围[b, e) 指向的元素,返回一个指向最后一个被删除元素下一位置的迭代器,若e 是尾后迭代器,则返回尾后迭代器 |
c.clear() |
清空容器c 中所有元素,返回void |
注意:删除元素的成员函数不检查其参数,在删除元素前需要保证被删除的元素是存在的。
特殊的forward_list
的操作
在单向链表中删除或者插入一个元素,被删除或者插入元素的前驱节点的链接会发生变化,但是单链表只能单向访问元素,无法从被删除的元素访问到其前驱节点,因此,对于forward_list
容器,删除或者插入元素是通过改变待删除或是插入元素的前驱节点实现的。
单链表forward_list
定义了一个首前迭代器before_begin()
,允许我们删除首元素或是在首元素之前插入元素。
下表是forward_list
支持的删除或是插入元素的操作
操作 | 含义 |
---|---|
lst.before_begin() |
返回lst 的首前迭代器,它不指向容器中的元素,不能解引用 |
lst.cbefore_begin() |
返回lst 的const 版本的首前迭代器 |
lst.insert_after(p, t) |
在迭代器p 后一位置插入一个值为t 的元素,返回一个被插入元素的迭代器 |
lst.inset_after(p, n , t) |
在迭代器p 后一位置开始插入n 个值为t 的元素,返回一个最新被插入的元素的迭代器 |
lst.insert_after(p, b, e) |
在迭代器p 后一位置开始插入由迭代器范围[b, e) 指向的元素,返回一个指向最新被插入元素的迭代器,若范围为空,则返回p |
lst.inset_after(p, il) |
在迭代器p 后一位置开始插入由{} 包围的元素列表il ,返回一个指向最新被插入的元素的迭代器,若il 为空,则返回p |
lst.emplace_after(p, inits) |
在迭代器p 后一位置插入由inits 在lst 内存空间中就地初始化的元素,返回一个指向最新被插入的元素的迭代器 |
lst.erease_after(p) |
删除迭代器p 后一位置的元素,返回一个指向被删除元素之后位置的迭代器 |
lst.erase_after(b, e) |
删除由迭代器范围[b, e) 指向的元素,返回一个指向最后一个被删除元素之后位置的迭代器,若不存在这样的元素,则返回尾后迭代器 |
注意:上述的和迭代器p
相关的操作,都需要程序员保证迭代器有效,否则函数行为未定义。
改变容器的大小
顺序容器支持的容器大小操作如下表所示。
操作 | 含义 |
---|---|
array 不支持容器的大小操作,因为array 容器的大小固定,是类型的一部分 |
|
c.resize(n) |
调整c 的大小为n 个元素,若n > c.size() ,则多余的元素会进行值初始化;若n < c.size() ,则多余的元素会被舍弃 |
c.resize(n, t) |
调整c 的大小为n 个元素,若n > c.size() ,则多余的元素的值会被初始化为值t ;若n < c.size() ,则多余的元素会被舍弃 |
vector
对象的空间增长策略
对于string
和vector
容器,因为容器要保证逻辑上相邻的元素在内存中也相邻,所以,当容器中没有空间能再容纳新元素时,向容器中插入新元素,将导致容器重新申请内存空间。为了保证效率,标准库实现时,申请新的内存空间后的总容量将比实际需要的容量要大。容器会将旧元素全部拷贝到新的内存空间中去,再在新的内存空间中插入新元素。
管理容量的成员函数
下表是顺序容器用来管理容器容量的成员函数。
操作 | 含义 |
---|---|
shrink_to_fit 只适用于vector 、deque 和string 容器,capacity 和reserve 只适用于vector 和string |
|
c.shink_to_fit |
将容器c 的容量缩小到与c.size() 相同大小,并不保证可以回退多余的空间,具体的实现可以忽略此请求 |
c.capacity() |
不重新分配内存空间的情况下当前容器c 可以保存的元素的最大数量 |
c.reserve(n) |
使容器c 分配至少能容纳n 个元素的内存空间,只有当n > c.size() 时才会重新分配内存空间 |
注意:vector
和string
的内存增长策略随着实现的不同而不同,通常的做法是:在需要重新分配内存空间时将当前的内存空间的容量翻倍。所有的内存空间分配策略遵循着一个原则:使用push_back
向容器中添加元素时有高效率,在一个初始为空的vector
上调用n
此push_back
来创建一个n
个元素的容器时,所花费的时间不能超过n
的常数倍。
容器操作可能使迭代器失效
向容器中插入或删除元素可能会导致指向容器元素的迭代器、指针或引用失效,使用失效的指针、引用或是迭代器会引发严重的程序错误。
容器中插入或删除元素后指向容器的迭代器、指针和引用的情况如下表。
容器类型 | 插入元素 | 删除元素 |
---|---|---|
vector 和string |
若重新分配内存空间,迭代器、指针和引用都会失效;未重新分配内存空间,指向插入元素位置之前的迭代器、引用和指针仍然有效,插入位置之后迭代器、指针和引用会失效 | 指向被删除元素之后的迭代器、指针和引用会失效 |
list 和forward_list |
全部有效 | 全部有效 |
deque |
在除首尾位置外的其他位置插入元素会导致迭代器、指针和引用失效;在首元素位置插入元素将会导致迭代器失效,但是指针和引用仍然有效 | 删除首元素,全部有效;删除尾元素,尾后迭代器失效;在首尾位置外的其他位置删除元素,会导致全部失效 |
注意:每次删除或插入元素后都要重新计算end()
返回的迭代器,不要使用end()
迭代器的保存值。
额外的string
操作
除了顺序容器的公共操作外,string
还定义了一些额外操作,这些操作要么提供了string
类和C
风格字符数组之间的相互转换,要么增加了使用下标替代迭代器的版本。
构造string
的其他方法
下表是除公共操作外的构造string
的其他方法。
操作 | 含义 |
---|---|
n、len、pos 都是无符号整型值 |
|
string s(cp, n) |
使用cp 指向的数组的前n 个字符拷贝初始化s ,cp 指向的数组应该至少包含n 个字符 |
string s(s2, pos) |
s 是string 类型的s2 的从下标pos 开始的字符的拷贝,若pos > s2.size() ,函数行为未定义 |
string s(s2, pos, len) |
s 是string 类型的s2 的从下标pos 开始的len 个字符的拷贝,若pos > s2.size() ,函数行为未定义,无论len 是多大,拷贝构造函数之多拷贝s2.size() - pos 个字符 |
substr
操作
下表是string
类型的substr
操作。
操作 | 含义 |
---|---|
s.substr(pos) |
返回一个string ,内容是s 的从下标pos 开始直到最后一个字符的string |
s.substr(pos, n) |
返回一个string ,内容是从s 的下标pos 开始的n 个字符,若n > s.size() - pos ,则至多拷贝s.size() - n 个字符 |