C++ STL学习

C++ STL学习

目录

容器库概览

对可以保存在容器中的元素的限制

容器需要支持拷贝操作,因此,保存在容器中的元素要能够支持拷贝操作。可以为不支持特定操作需求的元素类型定义容器,但是这样就只能使用容器那些不要求容器元素有特定操作的操作类型。

一般来说,每种容器都定义在一个与容器名相同的头文件中,对大多数容器,但不是所有容器,需要提供元素类型来生成特定的容器。

特别的,容器的元素的类型可以是另一种容器,只要在外层容器的<>中指定内层容器的类型即可。在老版本的编译器中,这种以其他容器为元素的容器,在声明或者定义时,需要在内层容器的<>和外层容器的<>之间插入空格,如vector<vector<int> > v1,新版本的编译器则无此要求。

容器支持的操作

容器类型支持的操作可以根据容器的大类分成一定的层次:

  1. 顺序容器和关联容器都支持的操作;
  2. 仅部分顺序容器支持的操作;
  3. 仅部分关联容器支持的操作;
  4. 少数特殊的容器,如arrayforward_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拷贝构造c1c2c1的容器类型和元素类型必须一样
C c1 = c2 使用c2拷贝构造c1c2c1的容器类型和元素类型必须一样
C c(b, e) 使用迭代器be的半开区间[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) 交换c1c2中的元素
swap(c1, c2) 等价于c1.swap(c2)
添加、删除元素
array不支持,在不同的容器中,接口可能不同)
c.insert(args) args中的元素拷贝进c
c.empalce(inits) 使用initsc中原地构造一个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 iter1iter2指向同一个容器中的同一个元素或尾后迭代器时成立
iter1 != iter2 等价于!(iter1 == iter2)

迭代器的类型

迭代器的const属性

所有的拥有迭代器的标准库类型使用两个关键字来表示迭代器类型:

  1. iterator表示通过此类迭代器可以对迭代器指向的元素进行读写;
  2. const_iterator表示通过此类型只能迭代器指向的元素进行读操作,不可改变迭代器指向的元素的值。

对于const类型的容器,只能使用const_iterator的迭代器。

迭代器的操作类型

可以按照迭代器的操作类型将迭代器分成5类,这5类迭代器形成一种操作层次:除输出迭代器外(输出迭代器可以看做输入迭代器在功能上的补集------输出迭代器只写而不读元素),高层次的迭代器支持低层次的迭器所支持的所有操作。它们所支持的操作范围可以用下列的不等式表示:

  1. 随机访问迭代器 > 双向迭代器 > 前向迭代器 > (输入迭代器(读元素),输出迭代器(写元素))
  2. 读写元素 = 输入迭代器(读元素)+ 输出迭代器(写元素)
迭代器类型 支持的操作
输入迭代器 除支持的公共操作外,支持前置的和后置的++,它使输入迭代器指向容器中的下一个元素位置。支持*解引用运算符,它只会出现在=右侧,用于读取元素。支持->箭头运算符,等价于(*it).mem,它解引用迭代器,并提取被指向对象的成员。输入迭代器只能顺序读取容器中的元素,递增迭代器可能使所有指向流的迭代器失效
输出迭代器 除支持的公共操作外,支持前置的和后置的++,它使输入迭代器指向容器中的下一个元素位置。支持*解引用运算符,它只会出现在=左侧,用于写元素。只能向一个输出迭代器赋值一次
前向迭代器 支持输入迭代器和输出迭代器的所有操作。只能在序列中沿着一个方向移动。可以多次读写同一个元素
双向迭代器 除支持前向迭代器的所有操作外,还支持前置的和后置的--运算符。可前向、后向移动,读写序列中的元素
随机访问迭代器 除支持双向迭代器的所有操作,还支持迭代器的算术运算。提供在常量时间内访问序列中任意元素的能力

随机访问迭代器支持的算术运算如下表所示。

算术运算类型 含义
iter + n 迭代器加上一个整型数,结果仍然是一个迭代器,指示的位置比iter指示的位置前进了n个单位,结果或是指向一个元素,或者一个尾后迭代器
iter - n 迭代器减去一个整型数,结果仍然是一个迭代器,指示的位置比iter指示的位置后退了n个单位,结果或是指向一个元素,或者一个尾后迭代器
iter += n 迭代器加法的复合赋值语句,将iter的值加上n后再赋值给iter
iter -= n 迭代器减法的复合赋值语句,将iter的值减去n后再赋值给iter
iter1 - iter2 迭代器相减的结果是它们之间的距离,表示ite2r向前移动差值个单位后将得到iter1。参与运算的两个迭代器必须是指向同一个容器的迭代器,或是同一个容器的尾后迭代器
>, >=, <, <= 迭代器的关系运算符,若迭代器指向的位置在另一个迭代器指向的位置之前,则说前者小于后者。参与关系运算的两个迭代器必须是指向同一个容器的迭代器,或是同一个容器的尾后迭代器

迭代器范围

一个迭代器范围由一对迭代器表示,这个两个迭代器分别指向同一个容器中的元素,或是同一个容器中的尾后位置。这两个迭代器分别用beginend表示(或是用firstlast表示),他们标记了一个容器中的一个元素范围。

通常endlast不会指向容器中的一个元素,而是指向容器中的尾后位置,这样的元素范围被称为左闭合区间,其数学描述为:[begin,end),表示范围自begin起,至end前结束,beginend可以指向相同的位置,但是不能指向容器首元素之前的位置。

使用左闭合区间的编程假定

标准库使用左闭合区间表示元素范围是因为左闭合区间有下面三种方便的性质,假定beginend表示一个合法的迭代器范围,则有:

  1. 范围为空当且仅当begin == end
  2. begin != end,则范围内至少存在一个元素;
  3. 可以通过递增begin使begin == end

顺序容器

顺序容器的特点是元素在容器中的相对位置和元素加入容器时的先后顺序有关,元素之间的逻辑结构是线型的

关联容器的特点是元素在容器中的相对位置和与每个元素绑定的关键字有关,元素之间的逻辑结构是非线型的,如树型结构、表型结构或者其他非线型结构。

顺序容器概述

顺序容器的类型和特点

C++标准库提供了如下的顺序容器。

容器类型 特点
vector 动态数组,逻辑上相邻的元素,在内存中也相邻。支持随机访问元素,不能在直接头部删除和添加元素(借助迭代器指向待删除的元素可以)。在一次添加、删除元素的操作后,在添加、删除位置之后的所有元素在内存中都要移动位置来保持元素连续存储的特点,在一次添加、删除元素后可能容器的容量会改变,所以元素要整体移动到内存中的新位置去,所以适合在尾部添加元素,在其他位置添加元素效率很低
string vector类似的容器,和vector之间的区别是string容器元素是字符
decque 双端队列,支持随机访问。在底层实现上,采用链表和动态数组的方式。在头部和尾部添加、删除元素效率高,在其他位置添加、删除元素效率低 。
list 双向链表,不支持随机访问,在逻辑上相邻的元素,在内存中不一定相邻。在任何位置添加、删除元素效率高
forward_list 单向链表,只支持从头部向尾部的单向遍历,不支持随机访问,在逻辑上相邻的元素,在内存中不一定相邻。在任何位置添加、删除元素的效率高。
array 固定大小的数组,支持随机访问,不能添加、删除元素

顺序容器在以下两个方面有不同程度的性能折中:

  1. 向容器中添加和删除元素的开销;
  2. 非顺序访问元素的开销。

确定使用哪种顺序容器

如无必要,使用vector容器,除非你有充分的理由选择其他容器。

在以下情况时可以考虑使用别的容器:

  1. 如果程序要求在容器中间添加、删除元素,使用list或者更forward_list
  2. 如果要求程序在头尾添加、删除元素,但是不会在中间添加、删除元素,使用deque

顺序容器的操作

顺序容器的定义和初始化

除所有容器都支持的容器的定义和初始化操作外,仅有顺序容器(除array外)还支持接受一个大小参数的构造函数,如下表所示。

构造函数 含义
C seq(n) seq包含了n个元素,这些元素进行了值初始化,此构造函数是explict属性的
C seq(n, t) seq包含了n个元素,这些元素通过t进行初始化

向顺序容器添加元素

下表是

操作 含义
向容器中添加元素的操作会改变容器的大小,array类型具有固定的大小,因此array不支持添加元素的操作。
vectorstrng不支持push_frontemplace_front操作。forward_list有自己版本的insertemplace操作,不支持push_backemplace_backemplace_frontpush_front操作
c.push_back(t) 使用tc的尾部拷贝构造一个元素
c.empalce_back(inits) 使用initsc的尾部就地构造一个元素
c.push_front(t) 使用tc的首元素拷贝构造一个元素,容器中原有的所以元素都要后移一个位置
c.emplace_front(inits) 使用initsc的头部就地构造一个元素,容器中原有的所以元素都要后移一个位置
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指向的位置前插入一个由{}包围的元素值列表中的元素,返回一个指向最新被插入的元素的迭代器
初始化和插入操作的关键概念
  1. 被插入容器中的元素是传递给容器的元素的拷贝,而不是传递给容器的元素本身,在初始化操作或者插入操作完成后,容器中的元素对象和提供值的元素之间就没有关系了,对容器中的元素对象的操作不会对提供值的元素对象产生影响,反之亦然。
  2. 在插入操作中,insertpush操作和emplace操作由效率上的区别。如果传递给容器的参数是容器元素的一个左值,则insertpush操作会调用容器元素的拷贝构造函数,将传给容器的对象拷贝给容器元素的对象来完成插入操作;如果传递给容器的参数是容器元素的一个右值,则可能会调用容器元素对象的移动构造函数(具体情况视编译器的实现而定),而emplace操作会直接调用与传入容器的对象相匹配的构造函数来在容器的内存空间中就地构造元素对象,没有了拷贝操作。
  3. 在使用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_backvector、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() 返回lstconst版本的首前迭代器
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后一位置插入由initslst内存空间中就地初始化的元素,返回一个指向最新被插入的元素的迭代器
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对象的空间增长策略

对于stringvector容器,因为容器要保证逻辑上相邻的元素在内存中也相邻,所以,当容器中没有空间能再容纳新元素时,向容器中插入新元素,将导致容器重新申请内存空间。为了保证效率,标准库实现时,申请新的内存空间后的总容量将比实际需要的容量要大。容器会将旧元素全部拷贝到新的内存空间中去,再在新的内存空间中插入新元素。

管理容量的成员函数

下表是顺序容器用来管理容器容量的成员函数。

操作 含义
shrink_to_fit只适用于vectordequestring容器,capacityreserve只适用于vectorstring
c.shink_to_fit 将容器c的容量缩小到与c.size()相同大小,并不保证可以回退多余的空间,具体的实现可以忽略此请求
c.capacity() 不重新分配内存空间的情况下当前容器c可以保存的元素的最大数量
c.reserve(n) 使容器c分配至少能容纳n个元素的内存空间,只有当n > c.size()时才会重新分配内存空间

注意:vectorstring的内存增长策略随着实现的不同而不同,通常的做法是:在需要重新分配内存空间时将当前的内存空间的容量翻倍。所有的内存空间分配策略遵循着一个原则:使用push_back向容器中添加元素时有高效率,在一个初始为空的vector上调用npush_back来创建一个n个元素的容器时,所花费的时间不能超过n的常数倍。

容器操作可能使迭代器失效

向容器中插入或删除元素可能会导致指向容器元素的迭代器、指针或引用失效,使用失效的指针、引用或是迭代器会引发严重的程序错误。

容器中插入或删除元素后指向容器的迭代器、指针和引用的情况如下表。

容器类型 插入元素 删除元素
vectorstring 若重新分配内存空间,迭代器、指针和引用都会失效;未重新分配内存空间,指向插入元素位置之前的迭代器、引用和指针仍然有效,插入位置之后迭代器、指针和引用会失效 指向被删除元素之后的迭代器、指针和引用会失效
listforward_list 全部有效 全部有效
deque 在除首尾位置外的其他位置插入元素会导致迭代器、指针和引用失效;在首元素位置插入元素将会导致迭代器失效,但是指针和引用仍然有效 删除首元素,全部有效;删除尾元素,尾后迭代器失效;在首尾位置外的其他位置删除元素,会导致全部失效

注意:每次删除或插入元素后都要重新计算end()返回的迭代器,不要使用end()迭代器的保存值。

额外的string操作

除了顺序容器的公共操作外,string还定义了一些额外操作,这些操作要么提供了string类和C风格字符数组之间的相互转换,要么增加了使用下标替代迭代器的版本。

构造string的其他方法

下表是除公共操作外的构造string的其他方法。

操作 含义
n、len、pos都是无符号整型值
string s(cp, n) 使用cp指向的数组的前n个字符拷贝初始化scp指向的数组应该至少包含n个字符
string s(s2, pos) sstring类型的s2的从下标pos开始的字符的拷贝,若pos > s2.size(),函数行为未定义
string s(s2, pos, len) sstring类型的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个字符

关联容器

泛型算法