C++ 系列 -- STL 标准库

STL

什么是 STL

STL,中文可译为标准模板库 或者泛型库,其包含有大量的模板类和模板函数,是 C++ 提供的一个基础模板的集合,用于完成诸如输入/输出、数学计算等功能。

STL 有什么用

以 C++ 定义数组的操作为例,在 C++ 中如果定义一个数组,可以采用如下方式:

cpp 复制代码
int a[n];

这种定义数组的方法需要事先确定好数组的长度,即 n 必须为常量,这意味着,如果在实际应用中无法确定数组长度,则一般会将数组长度设为可能的最大值,但这极有可能导致存储空间的浪费。

所以除此之外,还可以采用在堆空间中动态申请内存的方法,此时长度可以是变量:

cpp 复制代码
int *p = new int[n];

这种定义方式可根据变量 n 动态申请内存,不会出现存储空间浪费的问题。但是,如果程序执行过程中出现空间不足的情况时,则需要加大存储空间,此时需要进行如下操作:

  1. 新申请一个较大的内存空间,即执行int * temp = new int[m];

  2. 将原内存空间的数据全部复制到新申请的内存空间中,即执行memecpy(temp, p, sizeof(int)*n);

  3. 将原来的堆空间释放,即执行delete [] p; p = temp;

而完成相同的操作,如果采用 STL 标准库,则会简单很多 。下面是使用向量模板类 vector 实现以上功能的示例:

cpp 复制代码
// 定义 a 数组,当前数组长度为 0,
// 但和普通数组不同的是,此数组 a 可以根据存储数据的数量【自动变长】
vector <int> a;
// 向数组 a 中添加 10 个元素
for (int i = 0; i < 10 ; i++)
    a.push_back(i)
// 还可以手动调整数组 a 的大小
a.resize(100);
a[90] = 100;
// 还可以直接删除数组 a 中所有的元素,此时 a 的长度变为 0
a.clear();
// 重新调整 a 的大小为 20,并存储 20 个 -1 元素。
a.resize(20, -1)

总结来说:

STL 是 模板类模板函数 的集合

  • 模板类 = 容器(数组、对象等等放数据)
  • 模板函数 = 读写这些容器中数据的函数(算法)

之所以都要使用模板,是因为数据类型多样,必须使用泛型技术

STL 基本组成

通常认为,STL 是由容器、算法、迭代器、函数对象、适配器、内存分配器 这 6 部分构成,其中后面 4 部分是为前 2 部分服务的,它们各自的含义如下表所示

STL的组成 含义
容器 一些封装数据结构 的模板类,例如 vector 向量容器list 列表容器
算法 STL 提供了非常多(大约 100 个)的数据结构算法 ,它们都被设计成一个个的模板函数 ,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 <algorithm> 中,少部分位于头文件 <numeric>
迭代器 在 C++ STL 中,对容器中数据的读和写 ,是通过迭代器完成的,扮演着容器和算法之间的胶合剂
函数对象 如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)
适配器 可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器
内存分配器 为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用

C++ 标准库中有这 13 个标准头文件

  • <iterator>
  • <functional>
  • <vector>
  • <deque>
  • <list>
  • <queue>
  • <stack>
  • <set>
  • <map>
  • <algorithm>
  • <numeric>
  • <memory>
  • <utility>

STL 容器

STL 容器是什么

容器的简单理解:模板类的集合,但和普通模板类不同的是,容器中封装的是组织数据的方法(也就是数据结构)

STL 容器的分类

STL 提供有 3 类标准容器,分别是序列容器排序容器哈希容器,其中后两类容器有时也统称为关联容器

容器种类 功能
序列容器 主要包括 vector 向量容器、list 列表容器以及 deque 双端队列容器。之所以被称为序列容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序 的,顺序取决于插入顺序。将元素插入容器时,指定在什么位置,元素就会位于什么位置
排序容器 包括 set 集合容器、multiset 多重集合容器、map 映射容器以及 multimap 多重映射容器。排序容器中的元素默认是由小到大排序好的,即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能
哈希容器 包括 unordered_set 哈希集合、unordered_multiset 哈希多重集合、unordered_map 哈希映射以及 unordered_multimap 哈希多重映射。和排序容器不同,哈希容器中的元素是未排序的,元素的位置由哈希函数确定

以上 3 类容器的存储方式 完全不同,因此使用不同容器完成相同操作的效率 也大不相同。所以在实际使用时,要善于根据想实现的功能,选择合适的容器

STL 迭代器

STL 迭代器是什么

无论是序列容器还是关联容器,最常做的操作无疑是遍历容器中存储的元素,而实现此操作,多数情况会选用"迭代器(iterator)"来实现

我们知道,尽管不同容器的内部结构各异,但它们本质上都是用来存储大量数据的,换句话说,都是一串能存储多个数据的存储单元。因此,诸如数据的排序、查找、求和等需要对数据进行遍历操作方法应该是类似的。

既然类似,完全可以利用泛型技术,将它们设计成适用所有容器的通用算法,从而将容器和算法分离开。但实现此目的需要有一个类似中介 的装置,它除了要具有对容器进行遍历读写数据 的能力之外,还要能对外隐藏容器的内部差异,从而以统一的界面向算法传送数据

这是泛型思维发展的必然结果,于是迭代器就产生了

STL 迭代器分类

常用的迭代器按功能强弱分为输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器 5 种。

输入迭代器和输出迭代器比较特殊,它们不是把数组或容器当做操作对象,而是把输入流/输出流作为操作对象

我们先主要关注后面三种迭代器:

  1. 前向迭代器

    假设 p 是一个前向迭代器,则 p 支持 ++p,p++,*p 操作,还可以被复制或赋值,可以用 ==!= 运算符进行比较。此外,两个正向迭代器可以互相赋值。

  2. 双向迭代器 双向迭代器具有正向迭代器的全部功能,除此之外,假设 p 是一个双向迭代器,则还可以进行 --p 或者 p-- 操作(即一次向后移动一个位置)。

  3. 随机访问迭代器

    随机访问迭代器具有双向迭代器的全部功能。除此之外,假设 p 是一个随机访问迭代器,i 是一个整型变量或常量,则 p 还支持以下操作:

  • p+=i:使得 p 往后移动 i 个元素
  • p-=i:使得 p 往前移动 i 个元素
  • p+i:返回 p 后面第 i 个元素的迭代器
  • p-i:返回 p 前面第 i 个元素的迭代器
  • p[i]:返回 p 后面第 i 个元素的引用

此外,两个随机访问迭代器 p1、p2 还可以用 <、>、<=、>= 运算符进行比较。另外,表达式 p2-p1 也是有定义的,其返回值表示 p2 所指向元素和 p1 所指向元素的序号之差(也可以说是 p2 和 p1 之间的元素个数减一)

是 C++ 11 标准中不同容器指定使用的迭代器类型

容器 对应的迭代器类型
array 随机访问迭代器
vector 随机访问迭代器
deque 随机访问迭代器
list 双向迭代器
set / multiset 双向迭代器
map / multimap 双向迭代器
forward_list 前向迭代器
unordered_map / unordered_multimap 前向迭代器
unordered_set / unordered_multiset 前向迭代器
stack 不支持迭代器
queue 不支持迭代器

注意,容器适配器 stack 和 queue 没有迭代器,它们包含有一些成员函数,可以用来对元素进行访问

STL 迭代器有什么用

下面就以 vector 容器为例,带领大家实际感受迭代器的用法和功能。vector 支持随机访问迭代器,因此遍历 vector 容器有以下几种做法。下面的程序中,每个循环演示了一种做法:

cpp 复制代码
#include <iostream>
// 需要引入 vector 头文件
#include <vector>
using namespace std;
int main()
{
    vector<int> v{1,2,3,4,5,6,7,8,9,10}; // v 被初始化成有 10 个元素
    cout << "第一种遍历方法:" << endl;
    for (int i = 0; i < v.size(); ++i) // ① 像普通数组一样使用 vector 容器,size 返回元素个数
        cout << v[i] <<" ";
    
    // 创建一个正向迭代器,当然,vector也支持其他 3 种定义迭代器的方式
    vector<int>::iterator i;
    
    cout << endl << "第二种遍历方法:" << endl;
    for (i = v.begin(); i != v.end(); ++i) // ② 用 != 比较两个迭代器
        cout << *i << " ";
    
    cout << endl << "第三种遍历方法:" << endl;
    for (i = v.begin(); i < v.end(); ++i) // ③ 用 < 比较两个迭代器
        cout << *i << " ";
   
    cout << endl << "第四种遍历方法:" << endl;
    i = v.begin();
    while (i < v.end()) { // 间隔一个输出
        cout << *i << " ";
        i += 2; // 随机访问迭代器支持 "+= 整数"  的操作
    }
}

运行结果为:

第一种遍历方法:  
1 2 3 4 5 6 7 8 9 10  
第二种遍历方法:  
1 2 3 4 5 6 7 8 9 10  
第三种遍历方法:  
1 2 3 4 5 6 7 8 9 10  
第四种遍历方法:  
1 3 5 7 9

序列式容器

所谓序列容器,即以线性排列 (类似普通数组的存储方式)来存储某一指定类型(例如 int、double 等)的数据,需要特殊说明的是,该类容器并不会 自动对存储的元素按照值的大小进行排序

需要注意的是,序列容器只是一类容器的统称,并不指具体的某个容器,序列容器大致包含以下几类容器

  • array<T,N>数组容器 ):表示可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值;
  • vector<T>向量容器 ):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数);
  • deque<T>双端队列容器 ):和 vector 非常相似,区别在于使用该容器可以在头部和尾部高效插入和删除元素,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶;
  • list<T>链表容器 ):是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何位置 都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list<T> 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
  • forward_list<T>正向链表容器):和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。

array 容器

array 容器特点

array 容器是 C++ 11 标准中新增的序列容器,简单地理解,它就是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全(原因后续会讲),且效率并没有因此变差

和其它容器不同,array 容器的大小是固定的无法动态的扩展或收缩,这也就意味着,在使用该容器的过程无法借由增加或移除元素而改变其大小,它只允许访问或者替换存储的元素。

array 容器以类模板的形式定义在 <array> 头文件,并位于命名空间 std 中,如下所示:

cpp 复制代码
namespace std{
    template <typename T, size_t N>
    class array;
}

array 容器初始化

因此,在使用该容器之前,代码中需引入 <array> 头文件,并默认使用 std 命令空间,如下所示:

cpp 复制代码
#include <array>
using namespace std;

在 array<T,N> 类模板中,T 用于指明容器中的存储的具体数据类型,N 用于指明容器的大小,需要注意的是,这里的 N 必须是常量,不能用变量表示。

array 容器有多种初始化方式,如下代码展示了如何创建具有 10 个 double 类型元素的 array 容器:

cpp 复制代码
std::array<double, 10> values;

提示,如果程序中已经默认指定了 std 命令空间,这里可以省略 std::。

由此,就创建好了一个名为 values 的 array 容器,其包含 10 个浮点型元素。但是,由于未显式指定这 10 个元素的值,因此使用这种方式创建的容器中,各个元素的值是不确定的(array 容器不会做默认初始化操作)。

通过如下创建 array 容器的方式,可以将所有的元素初始化为 0 或者和默认元素类型等效的值:

cpp 复制代码
std::array<double, 10> values {};

使用该语句,容器中所有的元素都会被初始化为 0.0。

当然,在创建 array 容器的实例时,也可以像创建常规数组那样对元素进行初始化:

cpp 复制代码
std::array<double, 10> values {0.5,1.0,1.5,,2.0};

可以看到,这里只初始化了前 4 个元素,剩余的元素都会被初始化为 0.0。图 1 说明了这一点。

array 容器成员函数

除此之外,array 容器还提供有很多功能实用的成员函数,如下表

成员函数 功能
begin() 返回指向容器中第一个元素的随机访问迭代器。
end() 返回指向容器最后一个元素之后一个位置的随机访问迭代器,通常和 begin() 结合使用。
rbegin() 返回指向最后一个元素的随机访问迭代器。
rend() 返回指向第一个元素之前一个位置的随机访问迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
size() 返回容器中当前元素的数量,其值始终等于初始化 array 类的第二个模板参数 N。
max_size() 返回容器可容纳元素的最大数量,其值始终等于初始化 array 类的第二个模板参数 N。
empty() 判断容器是否为空,和通过 size()==0 的判断条件功能相同,但其效率可能更快。
at(n) 返回容器中 n 位置处元素的引用,该函数自动检查 n 是否在有效的范围内,如果不是则抛出 out_of_range 异常。
front() 返回容器中第一个元素的直接引用,该函数不适用于空的 array 容器。
back() 返回容器中最后一个元素的直接应用,该函数同样不适用于空的 array 容器。
data() 返回一个指向容器首个元素的指针。利用该指针,可实现复制容器中所有元素等类似功能。
fill(val) 将 val 这个值赋值给容器中的每个元素。
array1.swap(array2) 交换 array1 和 array2 容器中的所有元素,但前提是它们具有相同的长度和类型。

除此之外,C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 array 容器包含的 begin() 和 end() 成员函数不同的是,标准库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。

另外,在 <array> 头文件中还重载了 get() 全局函数,该重载函数的功能是访问容器中指定的元素,并返回该元素的引用。

正是由于 array 容器中包含了 at() 这样的成员函数,使得操作元素时比普通数组更安全。

下面代码演示了上表中一部分成员函数的用法和功能:

cpp 复制代码
#include <iostream>
//需要引入 array 头文件
#include <array>
using namespace std;
int main()
{
    std::array<int, 4> values{};
    //初始化 values 容器为 {0,1,2,3}
    for (int i = 0; i < values.size(); i++) {
        values.at(i) = i;
    }
    //使用 get() 重载函数输出指定位置元素
    cout << get<3>(values) << endl;
    //如果容器不为空,则输出容器中所有的元素
    if (!values.empty()) {
        for (auto val = values.begin(); val < values.end(); val++) {
            cout << *val << " ";
        }
    }
}

vector 容器(栈)

vector 容器是 STL 中最常用的容器之一,它和 array 容器非常类似,都可以看做是对 C++ 普通数组的"升级版"。不同之处在于,array 实现的是静态数组(容量固定的数组),而 vector 实现的是一个动态数组,即可以进行元素的插入和删除。

vector 常被称为向量容器,因为该容器擅长在尾部 插入或删除元素,在常量时间内就可以完成,时间复杂度为O(1);而对于在容器头部或者中部 插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶O(n)

创建 vector 容器

创建 vector 容器的方式有很多,大致可分为以下几种:(其中程序中已经默认指定了 std 命令空间,这里可以省略 std::)

  1. 创建一个空的存储 double 类型元素的一个 vector 容器:
cpp 复制代码
vector<double> values;

注意,这是一个空的 vector 容器,因为容器中没有元素,所以没有为其分配空间。当添加第一个元素(比如使用 push_back() 函数)时,vector 会自动分配内存。

在创建好空容器的基础上,还可以像下面这样通过调用 reserve() 成员函数来增加容器的容量:

cpp 复制代码
values.reserve(20);

这样就设置了容器的内存分配,即至少可以容纳 20 个元素。注意,如果 vector 的容量在执行此语句之前,已经大于或等于 20 个元素,那么这条语句什么也不做;另外,调用 reserve() 不会影响已存储的元素,也不会生成任何元素,即 values 容器内此时仍然没有任何元素。

还需注意的是,如果调用 reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector<T> 容器的元素可能已经被复制或移到了新的内存地址。所以后续再使用这些迭代器时,最好重新生成一下。

  1. 除了创建空 vector 容器外,还可以在创建的同时指定初始值以及元素个数,比如:
cpp 复制代码
vector<int> primes {2, 3, 5, 7, 11, 13, 17, 19};

这样就创建了一个含有 8 个素数的 vector 容器。

  1. 在创建 vector 容器时,也可以指定元素个数
cpp 复制代码
vector<double> values(20);

如此,values 容器开始时就有 20 个元素,它们的默认初始值都为 0。

注意,圆括号 () 和大括号 {} 是有区别的,前者(例如 (20) )表示元素的个数,而后者(例如 {20} ) 则表示 vector 容器中只有一个元素 20。

如果不想用 0 作为默认值,也可以指定一个其它值,例如:

cpp 复制代码
vector<double> values(20, 1.0);

第二个参数指定了所有元素的初始值,因此这 20 个元素的值都是 1.0。

值得一提的是,圆括号 () 中的 2 个参数,既可以是常量,也可以用变量来表示,例如:

cpp 复制代码
int num=20;
double value =1.0;
std::vector<double> values(num, value);
  1. 通过存储元素类型相同的其它 vector 容器,也可以创建新的 vector 容器,例如:
cpp 复制代码
vector<char>value1(5, 'c');
vector<char>value2(value1);

由此,value2 容器中也具有 5 个字符 'c'。在此基础上,如果不想复制其它容器中所有的元素,可以用一对指针或者迭代器来指定初始值的范围,例如:

cpp 复制代码
int array[]={1,2,3};
vector<int>values(array, array+2);//values 将保存{1,2}
vector<int>value1{1,2,3,4,5};
vector<int>value2(std::begin(value1),std::begin(value1)+3);//value2保存{1,2,3}

由此,value2 容器中就包含了 {1,2,3} 这 3 个元素。

vector 容器成员函数 ☆

函数成员 函数功能
begin() 返回指向容器中第一个元素的迭代器
end() 返回指向容器最后一个 元素所在位置后一个位置的迭代器,通常和 begin() 结合使用
rbegin() 返回指向最后一个元素的迭代器
rend() 返回指向第一个元素所在位置前一个位置的迭代器
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素
size() 返回实际元素个数
max_size() 返回元素个数的最大值这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数
resize() 改变实际元素的个数
capacity() 返回当前容量
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false
reserve() 增加 容器的容量
shrink_to_fit() 将内存减少到等于当前元素实际所使用的大小
operator[ ] 重载了 [ ] 运算符,可以向访问数组中元素那样,通过下标即可访问甚至修改 vector 容器中的元素
at() 使用经过边界检查的索引访问元素
front() 返回第一个 元素的引用
back() 返回最后一个 元素的引用
data() 返回指向容器中第一个 元素的指针
assign() 用新元素替换原有内容
push_back() 在序列的尾部添加一个元素
emplace_back() 在序列尾部生成一个元素
pop_back() 移出 序列尾部的元素
insert() 指定的位置插入一个或多个元素
emplace() 指定的位置 直接生成一个元素
erase() 移出一个 元素或一段元素
clear() 移出所有的元素,容器大小变为 0
swap() 交换两个容器的所有元素

部分成员函数的用法:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;
int main()
{
    // 初始化一个空vector容量
    vector<char>value;
    // 向 value 容器中的尾部依次添加 S、T、L 字符
    value.push_back('S');
    value.push_back('T');
    value.push_back('L');
    // 调用 size() 成员函数容器中的元素个数
    printf("元素个数为:%d\n", value.size());
    // 使用迭代器遍历容器
    for (auto i = value.begin(); i < value.end(); i++) {
        cout << *i << " ";
    }
    cout << endl;
    // 向容器开头插入字符
    value.insert(value.begin(), 'C');
    cout << "首个元素为:" << value.at(0) << endl;
    return 0;
}

vector 迭代器用法

vector 容器迭代器最常用的功能就是遍历访问容器中存储的元素。

首先来看 begin() 和 end() 成员函数,它们分别用于指向「首元素」和「尾元素+1」 的位置,下面程序演示了如何使用 begin() 和 end() 遍历 vector 容器并输出其中的元素:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int>values{1,2,3,4,5};
    auto first = values.begin();
    auto end = values.end();
    while (first != end)
    {
        *first = *first * 2; // 所有元素乘 2
        cout << *first << " ";
        ++first; // 逐步遍历
    }
    // first 等于 end 时遍历完毕
    return 0;
}

使用前缀++运算符对 first 进行自增,当 first 等于 end 时,所有的元素都被设完值 ,表示遍历完毕

cbegin()/cend() 成员函数和 begin()/end() 唯一不同的是,前者返回的是 const 类型的正向迭代器,这就意味着,由 cbegin() 和 cend() 成员函数返回的迭代器,可以用来遍历容器内的元素,也可以访问元素,但是不能对所存储的元素进行修改

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int>values{1,2,3,4,5};
    auto first = values.cbegin();
    auto end = values.cend();
    while (first != end)
    {
        // *first = 10; 不能修改元素
        cout << *first << " ";
        ++first;
    }
    return 0;
}

vector 模板类中还提供了 rbegin() 和 rend() 成员函数,分别表示指向最后一个元素和第一个元素前一个位置的随机访问迭代器,又称它们为反向迭代器(如图 2 所示)。

需要注意的是,在使用反向迭代器进行 ++ 或 -- 运算时,++ 指的是迭代器向左移动一位,-- 指的是迭代器向右移动一位,即这两个运算符的功能也"互换"了。

反向迭代器用于以逆序的方式遍历容器中的元素。例如:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int>values{1,2,3,4,5};
    auto first = values.rbegin();
    auto end = values.rend();
    while (first != end)
    {
        cout << *first << " ";
        ++first;
    }
    return 0;
}

运行结果为:

txt 复制代码
5 4 3 2 1

迭代器失效

每当 vector 容器的容量发生变化 时,我们都要对之前创建的迭代器重新初始化一遍

这是因为:values 容器在增加容量之后,首个元素的存储地址发生了改变,此时再使用先前创建的迭代器,显然是错误的

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;
int main()
{
    vector<int>values{1,2,3};
    cout << "values 容器首个元素的地址:" << values.data() << endl;
    auto first = values.begin();
    auto end = values.end();
    //增加 values 的容量
    values.reserve(20);
    cout << "values 容器首个元素的地址:" << values.data() << endl;
    first = values.begin();
    end = values.end();
    while (first != end) {
        cout << *first ;
        ++first;
    }
    return 0;
}

运行结果为:

txt 复制代码
values 容器首个元素的地址:0164DBE8  
values 容器首个元素的地址:01645560  
123

vector 容器访问/修改单个元素

  1. 容器名[n] 直接获取下标为 n 的元素,通过容器名.at(n)获取容量再和 n 作比较判断是否越界
  2. 容器名.at(n) 通过 at 成员函数获取下标为 n 的元素,当传给 at() 的索引会造成越界时,能够抛出 std::out_of_range 异常
  3. vector 容器还提供了 2 个成员函数,即 front()back(),它们分别返回 vector 容器中第一个最后一个元素引用

以上内容的示例如下:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> values{1,2,3,4,5};
    values[0] = values[1] + values[2] + values[3] + values[4]; // 修改容器下标为 0 的元素
    cout << values[0] << endl; // 获取容器中首个元素
    values.at(0) = values.at(1) + values.at(2) + values.at(3) + values.at(4); // 修改容器下标为 0 的元素
    cout << values.at(0) << endl; // 获取容器中首个元素
    values.front() = 10; // 修改首元素
    values.back() = 20; // 修改尾元素
    cout <<"values 新的首元素为:" << values.front() << endl; // 获取首元素
    cout << "values 新的尾元素为:" << values.back() << endl; // 获取尾元素
    cout << *(values.data() + 2) << endl; // 输出容器中第 3 个元素的值
    *(values.data() + 1) = 10; // 修改容器中第 2 个元素的值
    cout << *(values.data() + 1) << endl;
    return 0;
}

vector 容器访问多个元素

  1. 写 for 循环从 0 到 容器名.size() 之间任意多个
  2. 写基于范围的 for 循环
  3. 使用 vector 迭代器遍历 vector 容器
cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;
int main()
{
    vector<int> values{1,2,3,4,5};
    // 1. 从下标 0 一直遍历到 size()-1 处
    for (int i = 0; i < values.size(); i++) {
        cout << values[i] << " ";
    }
    // 2. 基于范围的 for 循环
    for (auto&& value : values)
        cout << value << " ";
    // 3. 使用 vector 迭代器
    for (auto first = values.begin(); first < values.end(); ++first) {
        cout << *first << " ";
    }
    return 0;
}

vector 容器尾部添加元素

  • push_back():先创建这个元素,然后再将这个元素拷贝或者移动到容器中
  • emplace_back():直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程(更推荐使用
cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;
int main()
{
    vector<int> values{};
    values.push_back(1);
    values.push_back(2);
    values.emplace_back(3);
    values.emplace_back(4);
    for (int i = 0; i < values.size(); i++) {
        cout << values[i] << " ";
    }
    return 0;
}

vector 容器插入元素

  • insert() :在 vector 容器的指定位置之前 插入一个或多个元素
  • emplace():在 vector 容器指定位置之前 插入一个 新的元素(在容器的指定位置直接构造元素,而不是先单独生成,再将其复制(或移动)到容器中,所以更推荐使用

insert()

语法格式 用法说明
insert(pos,elem) 在迭代器 pos 指定的位置之前插入一个新元素elem,并返回表示新插入元素位置的迭代器
insert(pos,n,elem) 在迭代器 pos 指定的位置之前插入 n 个元素 elem,并返回表示第一个新插入元素位置的迭代器
insert(pos,otherlist) 在迭代器 pos 指定的位置之前,插入初始化列表(用大括号{}括起来的多个元素,中间有逗号隔开)中所有的元素,并返回表示第一个新插入元素位置的迭代器
insert(pos,first,last) 在迭代器 pos 指定的位置之前,插入其他容器(不仅限于vector)中位于 [first,last) 区域的所有元素,并返回表示第一个新插入元素位置的迭代器
cpp 复制代码
#include <iostream> 
#include <vector> 
#include <array> 
using namespace std;

int main()
{
    std::vector<int> demo{1,2};
    // 第一种格式用法
    demo.insert(demo.begin() + 1, 3); // {1,3,2}

    // 第二种格式用法
    demo.insert(demo.end(), 2, 5); // {1,3,2,5,5}
    
    // 第三种格式用法
    demo.insert(demo.end(), { 10,11 }); // {1,3,2,5,5,7,8,9,10,11}

    // 第四种格式用法
    std::array<int,3>test{ 7,8,9 };
    demo.insert(demo.end(), test.begin(), test.end()); // {1,3,2,5,5,7,8,9}

    for (int i = 0; i < demo.size(); i++) {
        cout << demo[i] << " ";
    }
    return 0;
}

emplace()

语法格式 用法说明
emplace(pos,elem) 其中,pos 为指定插入位置的迭代器;args... 表示与新插入元素的构造函数相对应的多个参数;该函数会返回表示新插入元素位置的迭代器

简单的理解 args...,即被插入元素的构造函数需要多少个参数,那么在 emplace() 的第一个参数的后面,就需要传入相应数量的参数

cpp 复制代码
#include <vector>
#include <iostream>
using namespace std;

int main()
{
    std::vector<int> demo1{1,2,3,4,5};
    // emplace() 每次只能插入一个 int 类型元素
    demo1.emplace(demo1.begin() + 2, 666);
    for (int i = 0; i < demo1.size(); i++) {
        cout << demo1[i] << " "; // 1 2 666 3 4 5 
    }
    return 0;
}

vector 容器删除元素

函数 功能
pop_back() 删除 vector 容器中最后一个元素,容器 size 减 1,但容量不变
erase(pos) 删除 vector 容器中 pos 迭代器指定位置处的元素,并返回指向被删除元素下一个位置元素的迭代器。容器 size 减 1,但容量不变
swap(beg)pop_back() 先调用 swap() 函数交换要删除的目标元素和容器最后一个元素的位置,然后使用 pop_back() 删除该目标元素
erase(beg,end) 删除 vector 容器中位于迭代器 [beg,end) 指定区域内的所有元素,并返回指向被删除区域下一个位置元素的迭代器。容器 size 减小,但容量不变
remove() 删除容器中所有和指定元素值相等的元素,并返回指向最后一个元素下一个位置的迭代器。该函数不会改变容器的大小和容量
clear() 删除 vector 容器中所有的元素,使其变成空的 vector 容器。容器 size 减为 0,但容量不变

pop_back()

pop_back() 成员函数的用法非常简单,它不需要传入任何的参数,也没有返回值

cpp 复制代码
#include <vector>
#include <iostream>
using namespace std;

int main()
{
    vector<int>demo{ 1,2,3,4,5 };
    demo.pop_back();
    // 输出 dmeo 容器新的 size
    cout << "size is :" << demo.size() << endl;
    // 输出 demo 容器新的容量
    cout << "capacity is :" << demo.capacity() << endl;
    for (int i = 0; i < demo.size(); i++) {
        cout << demo[i] << " ";
    }
    return 0;
}

运行结果为:

txt 复制代码
size is :4  
capacity is :5  
1 2 3 4

可以发现,相比原 demo 容器,新的 demo 容器删除了最后一个元素 5,容器的大小减了 1,但容量没变。

erase (pos)

如果想删除 vector 容器中指定位置处的元素,可以使用 erase() 成员函数,该函数的语法格式为:

cpp 复制代码
iterator erase (pos);

其中,pos 为指定被删除元素 位置的迭代器 ,同时该函数会返回一个指向删除元素所在位置下一个位置迭代器

cpp 复制代码
#include <vector>
#include <iostream>
using namespace std;

int main()
{
    vector<int>demo{ 1,2,3,4,5 };
    auto iter = demo.erase(demo.begin() + 1);// 删除元素 2
    // 输出 dmeo 容器新的size
    cout << "size is :" << demo.size() << endl;
    // 输出 demo 容器新的容量
    cout << "capacity is :" << demo.capacity() << endl;
    for (int i = 0; i < demo.size(); i++) {
        cout << demo[i] << " ";
    }
    // iter 迭代器指向元素 3
    cout << endl << *iter << endl;
    return 0;
}

运行结果为:

txt 复制代码
size is :4  
capacity is :5  
1 3 4 5  
3

通过结果不能看出,erase() 函数在删除元素时,会将删除位置后续的元素陆续前移,并将容器的大小减 1。

swap(beg) + pop_back()

另外,如果不在意容器中元素的排列顺序,可以结合 swap() 和 pop_back() 函数,同样可以实现删除容器中指定位置元素的目的。

注意,swap() 函数在头文件 <algorithm><utility> 中都有定义,使用时引入其中一个即可。

cpp 复制代码
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
    vector<int>demo{ 1,2,3,4,5 };
    // 交换要删除元素和最后一个元素的位置
    swap(*(std::begin(demo)+1),*(std::end(demo)-1));//等同于 swap(demo[1],demo[4])
   
    // 交换位置后的 demo 容器
    for (int i = 0; i < demo.size(); i++) {
        cout << demo[i] << " ";
    }
    demo.pop_back();
    cout << endl << "size is :" << demo.size() << endl;
    cout << "capacity is :" << demo.capacity() << endl;
    // 输出 demo 容器中剩余的元素
    for (int i = 0; i < demo.size(); i++) {
        cout << demo[i] << " ";
    }
    return 0;
}

运行结果为:

txt 复制代码
1 5 3 4 2  
size is :4  
capacity is :5  
1 5 3 4

erase (first, last)

当然,除了删除容器中单个元素,还可以删除容器中某个指定区域内的所有元素,同样可以使用 erase() 成员函数实现。该函数有 2 种基本格式,前面介绍了一种,这里使用另一种:

cpp 复制代码
iterator erase (first, last);

其中 first 和 last 是指定被删除元素区域的迭代器,同时该函数会返回指向此区域之后一个位置的迭代器。

cpp 复制代码
#include <vector>
#include <iostream>
using namespace std;

int main()
{
    std::vector<int> demo{ 1,2,3,4,5 };
    //删除 2、3
    auto iter = demo.erase(demo.begin()+1, demo.end() - 2);
    cout << "size is :" << demo.size() << endl;
    cout << "capacity is :" << demo.capacity() << endl;

    for (int i = 0; i < demo.size(); i++) {
        cout << demo[i] << " ";
    }
    return 0;
}

运行结果为:

txt 复制代码
size is :3  
capacity is :5  
1 4 5

可以看到,和删除单个元素一样,删除指定区域内的元素时,也会将该区域后续的元素前移,并缩小容器的大小。

remove()

如果要删除容器中和指定元素值相同的所有元素,可以使用 remove() 函数,该函数定义在 <algorithm> 头文件中。例如:

cpp 复制代码
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
    vector<int>demo{ 1,3,3,4,3,5 };
    //交换要删除元素和最后一个元素的位置
    auto iter = std::remove(demo.begin(), demo.end(), 3);

    cout << "size is :" << demo.size() << endl;
    cout << "capacity is :" << demo.capacity() << endl;
    //输出剩余的元素
    for (auto first = demo.begin(); first < iter;++first) {
        cout << *first << " ";
    }
    return 0;
}

运行结果为:

txt 复制代码
size is :6  
capacity is :6  
1 4 5

注意,在对容器执行完 remove() 函数之后,由于该函数并没有改变容器原来的大小和容量,因此无法使用之前的方法遍历容器,而是需要向程序中那样,借助 remove() 返回的迭代器完成正确的遍历。

remove() 的实现原理是,在遍历容器中的元素时,一旦遇到目标元素,就做上标记,然后继续遍历,直到找到一个非目标元素,即用此元素将最先做标记的位置覆盖掉,同时将此非目标元素所在的位置也做上标记,等待找到新的非目标元素将其覆盖。因此,如果将上面程序中 demo 容器的元素全部输出,得到的结果为 1 4 5 4 3 5

另外还可以看到,既然通过 remove() 函数删除掉 demo 容器中的多个指定元素,该容器的大小和容量都没有改变,其剩余位置还保留了之前存储的元素。我们可以使用 erase() 成员函数删掉这些 "无用" 的元素。

比如,修改上面的程序:

cpp 复制代码
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
    vector<int>demo{ 1,3,3,4,3,5 };
    //交换要删除元素和最后一个元素的位置
    auto iter = std::remove(demo.begin(), demo.end(), 3);
    demo.erase(iter, demo.end());
    cout << "size is :" << demo.size() << endl;
    cout << "capacity is :" << demo.capacity() << endl;
    //输出剩余的元素
    for (int i = 0; i < demo.size();i++) {
        cout << demo[i] << " ";
    }
    return 0;
}

运行结果为:

txt 复制代码
size is :3  
capacity is :6  
1 4 5

remove()用于删除容器中指定元素时,常和 erase() 成员函数搭配使用。

deque 容器(队列)

deque 是 double-ended queue 的缩写,又称双端队列容器

和 vector 不同的是,deque 还擅长在序列头部 添加或删除元素,所耗费的时间复杂度也为常数阶O(1)。并且更重要的一点是,deque 容器中存储元素并不能保证所有元素都存储到连续的内存空间

当需要向序列两端频繁的添加或删除元素时,应首选 deque 容器。

创建 deque 容器

cpp 复制代码
deque<int> d; // 空队列
deque<int> d(10, 5) // 10 个元素值都为 5 的队列
deque<int> d1(5);
deque<int> d2(d1); // 拷贝该容器创建一个新的 deque 容器

deque 容器成员函数 ☆

函数成员 函数功能
begin() 返回指向容器中第一个元素的迭代器
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用
rbegin() 返回指向最后一个元素的迭代器
rend() 返回指向第一个元素所在位置前一个位置的迭代器
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素
size() 返回实际元素个数
max_size() 返回容器所能容纳元素个数的最大值。这通常是一个很大的值,一般是 232-1,我们很少会用到这个函数
resize() 改变实际元素的个数
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false
shrink_to_fit() 将内存减少到等于当前元素实际所使用的大小
at() 使用经过边界检查的索引访问元素
front() 返回第一个元素的引用
back() 返回最后一个元素的引用
assign() 用新元素替换原有内容
push_back() 在序列的尾部添加一个元素
push_front() 在序列的头部添加一个元素
pop_back() 移除容器尾部的元素
pop_front() 移除容器头部的元素
insert() 在指定的位置插入一个或多个元素
erase() 移除一个元素或一段元素
clear() 移出所有的元素,容器大小变为 0
swap() 交换两个容器的所有元素
emplace() 在指定的位置直接生成一个元素
emplace_front() 在容器头部生成一个元素。和 push_front() 的区别是,该函数直接在容器头部构造元素,省去了复制移动元素的过程
emplace_back() 在容器尾部生成一个元素。和 push_back() 的区别是,该函数直接在容器尾部构造元素,省去了复制移动元素的过程

和 vector 相比,额外增加了实现在容器头部添加和删除元素的成员函数,同时删除了 capacity()、reserve() 和 data() 成员函数。

和 array、vector 相同,C++ 11 标准库新增的 begin() 和 end() 这 2 个全局函数也适用于 deque 容器。这 2 个函数的操作对象既可以是容

deque 迭代器用法

cpp 复制代码
#include <iostream>
#include <deque>
using namespace std;
int main()
{
    deque<int>d{1,2,3,4,5};
    // 从容器首元素,遍历至最后一个元素
    for (auto i = d.begin(); i < d.end(); i++) {
        cout << *i << " ";
    }
    return 0;
}

迭代器失效

当向 deque 容器添加元素时,deque 容器会申请更多的内存空间,同时其包含的所有元素可能会被复制或移动到新的内存地址(原来占用的内存会释放),这会导致之前创建的迭代器失效。

cpp 复制代码
#include <iostream>
#include <deque>
using namespace std;
int main()
{
    deque<int>d;
    d.push_back(1);
    auto first = d.begin();
    cout << *first << endl;
    // 添加元素,会导致 first 失效
    d.push_back(1);
    // auto first = d.begin(); 得加这一行程序才不会报错
    cout << *first << endl; // 程序运行崩溃,因为迭代器 first 失效了
    return 0;
}

在对容器做添加元素的操作之后,如果仍需要使用之前以创建好的迭代器,为了保险起见,一定要重新生成。

deque 容器访问/修改的单个元素

  1. 容器名[n] 直接获取下标为 n 的元素,通过容器名.at(n)获取容量再和 n 作比较判断是否越界
  2. 容器名.at(n) 通过 at 成员函数获取下标为 n 的元素,当传给 at() 的索引会造成越界时,能够抛出 std::out_of_range 异常
  3. deque 容器还提供了 2 个成员函数,即 front()back(),它们分别返回 vector 容器中第一个最后一个元素引用

以上内容的示例如下:

cpp 复制代码
#include <iostream>
#include <deque>
using namespace std;

int main()
{
    deque<int> d{1,2,3,4,5};
    d[0] = d[1] + d[2] + d[3] + d[4]; // 修改容器下标为 0 的元素
    cout << d[0] << endl; // 获取容器中首个元素
    d.at(0) = d.at(1) + d.at(2) + d.at(3) + d.at(4); // 修改容器下标为 0 的元素
    cout << d.at(0) << endl; // 获取容器中首个元素
    d.front() = 10; // 修改首元素
    d.back() = 20; // 修改尾元素
    cout <<"d 新的首元素为:" << d.front() << endl; // 获取首元素
    cout << "d 新的尾元素为:" << d.back() << endl; // 获取尾元素
    return 0;
}

注意:和 vector 容器不同,deque 容器没有提供 data() 成员函数,同时 deque 容器在存储元素时,也无法保证 其会将元素存储在连续 的内存空间中,因此尝试使用指针去访问 deque 容器中指定位置处的元素,是非常危险

deque 容器添加和删除元素

deque 容器中,无论是添加元素还是删除元素,都只能借助 deque 模板类提供的成员函数。表 1 中罗列的是所有和添加或删除容器内元素相关的 deque 模板类中的成员函数。

成员函数 功能
push_back() 在容器现有元素的尾部添加一个元素,和 emplace_back() 不同,该函数添加新元素的过程是,先构造元素,然后再将该元素移动或复制到容器的尾部
pop_back() 移除容器尾部的一个元素
push_front() 在容器现有元素的头部添加一个元素,和 emplace_back() 不同,该函数添加新元素的过程是,先构造元素,然后再将该元素移动或复制到容器的头部
pop_front() 移除容器尾部的一个元素
emplace_back() C++ 11 新添加的成员函数,其功能是在容器尾部生成一个元素。和 push_back() 不同,该函数直接在容器头部构造元素,省去了复制或移动元素的过程
emplace_front() C++ 11 新添加的成员函数,其功能是在容器头部生成一个元素。和 push_front() 不同,该函数直接在容器头部构造元素,省去了复制或移动元素的过程
insert() 在指定的位置直接生成一个元素。和 emplace() 不同的是,该函数添加新元素的过程是,先构造元素,然后再将该元素移动或复制到容器的指定位置
emplace() C++ 11 新添加的成员函数,其功能是 insert() 相同,即在指定的位置直接生成一个元素。和 insert() 不同的是,emplace() 直接在容器指定位置构造元素,省去了复制或移动元素的过程
erase() 移除一个元素或某一区域内的多个元素
clear() 删除容器中所有的元素

在实际应用中,常用 emplace()、emplace_front() 和 emplace_back() 分别代替 insert()、push_front() 和 push_back(),具体原因本节后续会讲。

以上这些成员函数中,除了 insert() 函数的语法格式比较多,其他函数都只有一种用法(erase() 有 2 种语法格式),下面这段程序演示了它们的具体用法:

cpp 复制代码
#include <deque>
#include <iostream>
using namespace std;
int main()
{
    deque<int>d;
    //调用push_back()向容器尾部添加数据。
    d.push_back(2); //{2}
    //调用pop_back()移除容器尾部的一个数据。
    d.pop_back(); //{}

    //调用push_front()向容器头部添加数据。
    d.push_front(2);//{2}
    //调用pop_front()移除容器头部的一个数据。
    d.pop_front();//{}

    //调用 emplace 系列函数,向容器中直接生成数据。
    d.emplace_back(2); //{2}
    d.emplace_front(3); //{3,2}
    //emplace() 需要 2 个参数,第一个为指定插入位置的迭代器,第二个是插入的值。
    d.emplace(d.begin() + 1, 4);//{3,4,2}
    for (auto i : d) {
        cout << i << " ";
    }
    //erase()可以接受一个迭代器表示要删除元素所在位置
    //也可以接受 2 个迭代器,表示要删除元素所在的区域。
    d.erase(d.begin());//{4,2}
    d.erase(d.begin(), d.end());//{},等同于 d.clear()
    return 0;
}

运行结果为:

3 4 2

这里重点讲一下 insert() 函数的用法。insert() 函数的功能是在 deque 容器的指定位置插入一个或多个元素。该函数的语法格式有多种,如表 2 所示。

语法格式 功能
insert(pos,elem) 在迭代器 pos 指定的位置之前插入一个新元素elem,并返回表示新插入元素位置的迭代器
insert(pos,n,elem) 在迭代器 pos 指定的位置之前插入 n 个元素 elem,并返回表示第一个新插入元素位置的迭代器
insert(pos,first,last) 在迭代器 pos 指定的位置之前,插入其他容器(不仅限于vector)中位于 [first,last) 区域的所有元素,并返回表示第一个新插入元素位置的迭代器
insert(pos,initlist) 在迭代器 pos 指定的位置之前,插入初始化列表(用大括号{}括起来的多个元素,中间有逗号隔开)中所有的元素,并返回表示第一个新插入元素位置的迭代器

下面的程序演示了 insert() 函数的这几种用法:

cpp 复制代码
#include <iostream>
#include <deque>
#include <array>
using namespace std;
int main()
{
    std::deque<int> d{ 1,2 };
    // 第一种格式用法
    d.insert(d.begin() + 1, 3);//{1,3,2}

    // 第二种格式用法
    d.insert(d.end(), 2, 5);//{1,3,2,5,5}

    // 第三种格式用法
    std::array<int, 3>test{ 7,8,9 };
    d.insert(d.end(), test.begin(), test.end());//{1,3,2,5,5,7,8,9}

    // 第四种格式用法
    d.insert(d.end(), { 10,11 });//{1,3,2,5,5,7,8,9,10,11}

    for (int i = 0; i < d.size(); i++) {
        cout << d[i] << " ";
    }
    return 0;
}

运行结果为:

txt 复制代码
1,3,2,5,5,7,8,9,10,11

emplace系列函数的优势

有关 emplace()、emplace_front() 和 emplace_back() 分别和 insert()、push_front() 和 push_back() 在运行效率上的对比,可以通过下面的程序体现出来:

cpp 复制代码
#include <deque>
#include <iostream>
using namespace std;
class testDemo
{
public:
    testDemo(int num) :num(num) {
        std::cout << "调用构造函数" << endl;
    }
    testDemo(const testDemo& other) :num(other.num) {
        std::cout << "调用拷贝构造函数" << endl;
    }
    testDemo(testDemo&& other) :num(other.num) {
        std::cout << "调用移动构造函数" << endl;
    }
    testDemo& operator=(const testDemo& other);
private:
    int num;
};

testDemo& testDemo::operator=(const testDemo& other) {
    this->num = other.num;
    return *this;
}
int main()
{
    //emplace和insert
    cout << "emplace:" << endl;
    std::deque<testDemo> demo1;
    demo1.emplace(demo1.begin(), 2);
    cout << "insert:" << endl;
    std::deque<testDemo> demo2;
    demo2.insert(demo2.begin(), 2);
   
    //emplace_front和push_front
    cout << "emplace_front:" << endl;
    std::deque<testDemo> demo3;
    demo3.emplace_front(2);
    cout << "push_front:" << endl;
    std::deque<testDemo> demo4;
    demo4.push_front(2);

    //emplace_back()和push_back()
    cout << "emplace_back:" << endl;
    std::deque<testDemo> demo5;
    demo5.emplace_back(2);

    cout << "push_back:" << endl;
    std::deque<testDemo> demo6;
    demo6.push_back(2);
    return 0;
}

运行结果为:

txt 复制代码
emplace:  
调用构造函数  
insert:  
调用构造函数  
调用移动构造函数  
emplace_front:  
调用构造函数  
push_front:  
调用构造函数  
调用移动构造函数  
emplace_back:  
调用构造函数  
push_back:  
调用构造函数  
调用移动构造函数

可以看到,相比和它同功能的函数,emplace 系列函数都只调用了构造函数,而没有调用移动构造函数,这无疑提高了代码的运行效率。

list 容器(双向链表)

list 容器,又称双向链表容器,即该容器的底层是以双向链表的形式实现的。这意味着,list 容器中的元素可以分散存储在内存空间里,而不是必须存储在一整块连续的内存空间中。

下图展示了 list 双向链表容器是如何存储元素的

使用 list 容器的缺点 是,它不能像 array 和 vector 那样,通过位置直接访问元素。举个例子,如果要访问 list 容器中的第 6 个元素,它不支持 容器对象名[6]这种语法格式,正确的做法是从容器中第一个元素或最后一个元素开始遍历容器,直到找到该位置。

实际场景中,如何需要对序列进行大量添加或删除元素 的操作,而直接访问 元素的需求却很少,这种情况建议使用 list 容器存储序列

创建 list 容器

cpp 复制代码
list<int> values; // 空双向链表
list<int> values(10, 5) // 10 个元素值都为 5 的双向链表
list<int> values1(5);
list<int> values2(values1); // 拷贝该容器创建一个新的 list 容器

list 容器成员函数 ☆

成员函数 功能
begin() 返回指向容器中第一个元素的双向迭代器
end() 返回指向容器中最后一个元素所在位置的下一个位置的双向迭代器
rbegin() 返回指向最后一个元素的反向双向迭代器
rend() 返回指向第一个元素所在位置前一个位置的反向双向迭代器
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false
size() 返回当前容器实际包含的元素个数
max_size() 返回容器所能包含元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数
front() 返回第一个元素的引用
back() 返回最后一个元素的引用
assign() 用新元素替换容器中原有内容
emplace_front() 在容器头部生成一个元素。该函数和 push_front() 的功能相同,但效率更高
push_front() 在容器头部插入一个元素
pop_front() 删除容器头部的一个元素
emplace_back() 在容器尾部直接生成一个元素。该函数和 push_back() 的功能相同,但效率更高
push_back() 在容器尾部插入一个元素
pop_back() 删除容器尾部的一个元素
emplace() 在容器中的指定位置插入元素。该函数和 insert() 功能相同,但效率更高
insert() 在容器中的指定位置插入元素
erase() 删除容器中一个或某区域内的元素
swap() 交换两个容器中的元素,必须保证这两个容器中存储的元素类型是相同的
resize() 调整容器的大小
clear() 删除容器存储的所有元素
splice() 将一个 list 容器中的元素插入到另一个容器的指定位置
remove(val) 删除容器中所有等于 val 的元素
remove_if() 删除容器中满足条件的元素
unique() 删除容器中相邻的重复元素,只保留一个
merge() 合并两个事先已排好序的 list 容器,并且合并之后的 list 容器依然是有序的
sort() 通过更改容器中元素的位置,将它们进行排序
reverse() 反转容器中元素的顺序

list 容器添加/插入元素

前面章节介绍了如何创建 list 容器,在此基础上,本节继续讲解如何向现有 list 容器中添加或插入新的元素。

list 模板类中,与"添加或插入新元素"相关的成员方法有如下几个:

  • push_front():向 list 容器首个元素前添加新元素;

  • push_back():向 list 容器最后一个元素后添加新元素;

  • emplace_front():在容器首个元素前直接生成新的元素;

  • emplace_back():在容器最后一个元素后直接生成新的元素;

  • emplace():在容器的指定位置直接生成新的元素;

  • insert():在指定位置插入新元素;

  • splice():将其他 list 容器存储的多个元素添加到当前 list 容器的指定位置处。

以上这些成员方法中,除了 insert() 和 splice() 方法有多种语法格式外,其它成员方法都仅有 1 种语法格式,下面程序演示了它们的具体用法。

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;

int main()
{
    std::list<int> values{1,2,3};
    values.push_front(0);//{0,1,2,3}
    values.push_back(4); //{0,1,2,3,4}

    values.emplace_front(-1);//{-1,0,1,2,3,4}
    values.emplace_back(5);  //{-1,0,1,2,3,4,5}
   
    //emplace(pos,value),其中 pos 表示指明位置的迭代器,value为要插入的元素值
    values.emplace(values.end(), 6);//{-1,0,1,2,3,4,5,6}
    for (auto p = values.begin(); p != values.end(); ++p) {
        cout << *p << " ";
    }
    return 0;
}

输出结果为:

txt 复制代码
-1,0,1,2,3,4,5,6

list insert()成员方法

insert() 成员方法的语法格式有 4 种,如表 1 所示。

语法格式 用法说明
insert(pos,elem) 在迭代器 pos 指定的位置之前插入一个新元素 elem,并返回表示新插入元素位置的迭代器
insert(pos,n,elem) 在迭代器 pos 指定的位置之前插入 n 个元素 elem,并返回表示第一个新插入元素位置的迭代器
insert(pos,first,last) 在迭代器 pos 指定的位置之前,插入其他容器(例如 array、vector、deque 等)中位于 [first,last) 区域的所有元素,并返回表示第一个新插入元素位置的迭代器
insert(pos,initlist) 在迭代器 pos 指定的位置之前,插入初始化列表(用大括号 { } 括起来的多个元素,中间有逗号隔开)中所有的元素,并返回表示第一个新插入元素位置的迭代器

下面的程序演示了如何使用 insert() 方法向 list 容器中插入元素。

cpp 复制代码
#include <iostream>
#include <list>
#include <array>
using namespace std;
int main()
{
    std::list<int> values{ 1,2 };
    //第一种格式用法
    values.insert(values.begin() , 3);//{3,1,2}

    //第二种格式用法
    values.insert(values.end(), 2, 5);//{3,1,2,5,5}

    //第三种格式用法
    std::array<int, 3>test{ 7,8,9 };
    values.insert(values.end(), test.begin(), test.end());//{3,1,2,5,5,7,8,9}

    //第四种格式用法
    values.insert(values.end(), { 10,11 });//{3,1,2,5,5,7,8,9,10,11}

    for (auto p = values.begin(); p != values.end(); ++p)
    {
        cout << *p << " ";
    }
    return 0;
}

输出结果为:

txt 复制代码
3 1 2 5 5 7 8 9 10 11

学到这里,读者有没有发现,同样是实现插入元素的功能,无论是 push_front()、push_back() 还是 insert(),都有以 emplace 为名且功能和前者相同的成员函数。这是因为,后者是 C++ 11 标准新添加的,在大多数场景中,都可以完全替代前者实现同样的功能。更重要的是,实现同样的功能,emplace 系列方法的执行效率更高。

有关 list 模板类中 emplace 系列函数执行效率更高的原因,前面在讲解 deque 容器模板类中的 emplace 系列函数时已经讲过,读者可阅读《C++ STL deque容器添加和删除元素》一节做详细了解。

list splice()成员方法

和 insert() 成员方法相比,splice() 成员方法的作用对象是其它 list 容器,其功能是将其它 list 容器中的元素添加到当前 list 容器中指定位置处。

splice() 成员方法的语法格式有 3 种,如表 2 所示。

语法格式 功能
void splice (position, list& x); position 为迭代器,用于指明插入位置;x 为另一个 list 容器。 此格式的 splice() 方法的功能是,将 x 容器中存储的所有元素全部移动当前 list 容器中 position 指明的位置处。
void splice (position, list& x, i); position 为迭代器,用于指明插入位置;x 为另一个 list 容器;i 也是一个迭代器,用于指向 x 容器中某个元素。 此格式的 splice() 方法的功能是将 x 容器中 i 指向的元素移动到当前容器中 position 指明的位置处。
void splice (position, list& x, first, last); position 为迭代器,用于指明插入位置;x 为另一个 list 容器;first 和 last 都是迭代器,[fist,last) 用于指定 x 容器中的某个区域。 此格式的 splice() 方法的功能是将 x 容器 [first, last) 范围内所有的元素移动到当前容器 position 指明的位置处。

我们知道,list 容器底层使用的是链表存储结构,splice() 成员方法移动元素的方式是,将存储该元素的节点从 list 容器底层的链表中摘除,然后再链接到当前 list 容器底层的链表中。这意味着,当使用 splice() 成员方法将 x 容器中的元素添加到当前容器的同时,该元素会从 x 容器中删除。

下面程序演示了 splice() 成员方法的用法:

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;
int main()
{
    //创建并初始化 2 个 list 容器
    list<int> mylist1{ 1,2,3,4 }, mylist2{10,20,30};
    list<int>::iterator it = ++mylist1.begin(); //指向 mylist1 容器中的元素 2
   
    //调用第一种语法格式
    mylist1.splice(it, mylist2); // mylist1: 1 10 20 30 2 3 4
                                 // mylist2:
                                 // it 迭代器仍然指向元素 2,只不过容器变为了 mylist1

    //调用第二种语法格式,将 it 指向的元素 2 移动到 mylist2.begin() 位置处
    mylist2.splice(mylist2.begin(), mylist1, it);   // mylist1: 1 10 20 30 3 4
                                                    // mylist2: 2
                                                    // it 仍然指向元素 2
   
    //调用第三种语法格式,将 [mylist1.begin(),mylist1.end())范围内的元素移动到 mylist.begin() 位置处                  
    mylist2.splice(mylist2.begin(), mylist1, mylist1.begin(), mylist1.end());//mylist1:
                                                                             //mylist2:1 10 20 30 3 4 2
   
    cout << "mylist1 包含 " << mylist1.size() << " 个元素" << endl;
    cout << "mylist2 包含 " << mylist2.size() << " 个元素" << endl;
    //输出 mylist2 容器中存储的数据
    cout << "mylist2:";
    for (auto iter = mylist2.begin(); iter != mylist2.end(); ++iter) {
        cout << *iter << " ";
    }
    return 0;
}

程序执行结果为:

txt 复制代码
mylist1 包含 0 个元素  
mylist2 包含 7 个元素  
mylist2:1 10 20 30 3 4 2

list 容器删除元素

对 list 容器存储的元素执行删除操作,需要借助该容器模板类提供的成员函数。幸运的是,相比其它 STL 容器模板类,list 模板类提供了更多用来实现此操作的成员函数(如表 1 所示)。

成员函数 功能
pop_front() 删除位于 list 容器头部的一个元素
pop_back() 删除位于 list 容器尾部的一个元素
erase() 该成员函数既可以删除 list 容器中指定位置处的元素,也可以删除容器中某个区域内的多个元素
clear() 删除 list 容器存储的所有元素
remove(val) 删除容器中所有等于 val 的元素
unique() 删除容器中相邻的重复元素,只保留一份
remove_if() 删除容器中满足条件的元素

其中,pop_front()、pop_back() 和 clear() 的用法非常简单,这里仅给出一个样例,不再过多解释:

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;
int main()
{
    list<int>values{ 1,2,3,4 };
   
    // 删除当前容器中首个元素
    values.pop_front(); // {2,3,4}
   
    // 删除当前容器最后一个元素
    values.pop_back(); // {2,3}
   
    // 清空容器,删除容器中所有的元素
    values.clear(); // {}
   
    for (auto begin = values.begin(); begin != values.end(); ++begin)
    {
        cout << *begin << " ";
    }
    return 0;
}

运行程序,可以看到输出结果为"空"。

erase() 成员函数有以下 2 种语法格式:

cpp 复制代码
erase (position);  
erase (first, last);

利用第一种语法格式,可实现删除 list 容器中 position 迭代器所指位置处的元素,例如:

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;
int main()
{
    list<int>values{ 1,2,3,4,5 };
    // 指向元素 1 的迭代器
    auto del = values.begin();
    // 迭代器右移,改为指向元素 2
    ++del;
    values.erase(del); // {1,3,4,5}

    for (auto begin = values.begin(); begin != values.end(); ++begin)
    {
        cout << *begin << " ";
    }
    return 0;
}

运行结果为:

txt 复制代码
1 3 4 5

利用第二种语法格式,可实现删除 list 容器中 first 迭代器和 last 迭代器限定区域内的所有元素(包括 first 指向的元素,但不包括 last 指向的元素)。例如:

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;
int main()
{
    list<int>values{ 1,2,3,4,5 };
    //指定删除区域的左边界
    auto first = values.begin();
    ++first;//指向元素 2

    //指向删除区域的右边界
    auto last = values.end();
    --last;//指向元素 5

    //删除 2、3 和 4
    values.erase(first, last);

    for (auto begin = values.begin(); begin != values.end(); ++begin)
    {
        cout << *begin << " ";
    }
    return 0;
}

运行结果为:

txt 复制代码
1 5

erase() 成员函数是按照被删除元素所在的位置来执行删除操作,如果想根据元素的值来执行删除操作,可以使用 remove() 成员函数。例如:

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;
int main()
{
    list<char>values{'a','b','c','d'};
    values.remove('c');
    for (auto begin = values.begin(); begin != values.end(); ++begin)
    {
        cout << *begin << " ";
    }
    return 0;
}

运行结果为:

txt 复制代码
a b d

unique() 函数也有以下 2 种语法格式:

cpp 复制代码
void unique()  
void unique(BinaryPredicate)//传入一个二元谓词函数

以上 2 种格式都能实现去除 list 容器中相邻重复的元素,仅保留一份。但第 2 种格式的优势在于,我们能自定义去重的规则,例如:

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;
//二元谓词函数
bool demo(double first, double second)
{
    return (int(first) == int(second));
}

int main()
{
    list<double> mylist{ 1,1.2,1.2,3,4,4.5,4.6 };
    //删除相邻重复的元素,仅保留一份
    mylist.unique();//{1, 1.2, 3, 4, 4.5, 4.6}

    for (auto it = mylist.begin(); it != mylist.end(); ++it)
        cout << *it << ' ';
    cout << endl;
    //demo 为二元谓词函数,是我们自定义的去重规则
    mylist.unique(demo);

    for (auto it = mylist.begin(); it != mylist.end(); ++it)
        std::cout << *it << ' ';
    return 0;
}

运行结果为:

txt 复制代码
1 1.2 3 4 4.5 4.6  
1 3 4

可以看到,通过调用无参的 unique(),仅能删除相邻重复 的元素,而通过我们自定义去重的规则,可以更好的满足在不同场景下去重的需求。

除此之外,通过将自定义的谓词函数(不限定参数个数)传给 remove_if() 成员函数,list 容器中能使谓词函数成立的元素都会被删除。举个例子:

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;

int main()
{
    std::list<int> mylist{ 15, 36, 7, 17, 20, 39, 4, 1 };
    // 删除 mylist 容器中能够使 lamba 表达式成立的所有元素。
    mylist.remove_if([](int value) {return (value < 10); }); // {15 36 17 20 39}

    for (auto it = mylist.begin(); it != mylist.end(); ++it)
        std::cout << ' ' << *it;

    return 0;
}

运行结果为:

txt 复制代码
15 36 17 20 39

无序关联式容器(哈希容器)

无序关联式容器和关联式容器有本质上的不同:

  • 关联式容器的底层实现采用的树存储结构,更确切的说是红黑树结构;
  • 无序容器的底层实现采用的是哈希表的存储结构。

和关联式容器相比,无序容器具有以下两个特点

  1. 无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键
  2. 和关联式容器相比,无序容器擅长通过指定键查找对应的值(平均时间复杂度为 O(1));但对于使用迭代器遍历容器中存储的元素,无序容器的执行效率则不如关联式容器。

无序容器的分类

无序容器 功能
unordered_map 存储键值对 <key, value> 类型的元素,其中各个键值对键的值不允许重复 ,且该容器中存储的键值对是无序
unordered_set 不再以键值对的形式存储数据,而是直接存储数据元素本身(当然也可以理解为,该容器存储的全部都是键 key 和值 value 相等的键值对,正因为它们相等,因此只存储 value 即可)另外,该容器存储的元素不能重复,且容器内部存储的元素也是无序的
unordered_multimap 和 unordered_map 唯一的区别在于,该容器允许存储多个键相同的键值对
unordered_multiset 和 unordered_set 唯一的区别在于,该容器允许存储值相同的元素

unordered_map 容器

创建 unordered_map 容器

使用前引入头文件和规定命名空间

cpp 复制代码
#include <unordered_map>
using namespace std;

创建及初始化

cpp 复制代码
unordered_map<string, string> umap; // 空的 unordered_map 容器
// 创建的同时初始化
unordered_map<string, string> umap{
    {"Python教程","http://c.biancheng.net/python/"},
    {"Java教程","http://c.biancheng.net/java/"},
    {"Linux教程","http://c.biancheng.net/linux/"}
};

unordered_map 迭代器

成员方法 功能
begin() 返回指向容器中第一个键值对的正向迭代器
end() 返回指向容器中最后一个键值对之后位置的正向迭代器
cbegin() 和 begin() 功能相同,只不过在其基础上增加了 const 属性,即该方法返回的迭代器不能用于修改容器内存储的键值对
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,即该方法返回的迭代器不能用于修改容器内存储的键值对
find(key) 查找以 key 为键的键值对,如果找到,则返回一个指向该键值对的正向迭代器;反之,则返回一个指向容器中最后一个键值对之后位置的迭代器(如果 end() 方法返回的迭代器)
equal_range(key) 返回一个 pair 对象,其包含 2 个迭代器,用于表明当前容器中键为 key 的键值对所在的范围
cpp 复制代码
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main()
{
    // 创建 umap 容器
    unordered_map<string, string> umap{
        {"Python教程","http://c.biancheng.net/python/"},
        {"Java教程","http://c.biancheng.net/java/"},
        {"Linux教程","http://c.biancheng.net/linux/"}
    };

    cout << "umap 存储的键值对包括:" << endl;
    // 遍历输出 umap 容器中所有的键值对
    for (auto iter = umap.begin(); iter != umap.end(); ++iter) {
        cout << "<" << iter->first << ", " << iter->second << ">" << endl;
    }
    // 获取指向指定键值对的前向迭代器
    unordered_map<string, string>::iterator iter = umap.find("Java教程");
    cout <<"umap.find(\"Java教程\") = " << "<" << iter->first << ", " << iter->second << ">" << endl;
    return 0;
}

执行结果为:

txt 复制代码
umap 存储的键值对包括:  
<Python教程, http://c.biancheng.net/python/>  
<Linux教程, http://c.biancheng.net/linux/>  
<Java教程, http://c.biancheng.net/java/>  
umap.find("Java教程") = <Java教程, http://c.biancheng.net/java/>

unordered_map 容器成员函数 ☆

成员方法 功能
empty() 若容器为空,则返回 true;否则 false
size() 返回当前容器中存有键值对的个数
max_size() 返回容器所能容纳键值对的最大个数,不同的操作系统,其返回值亦不相同
operator[key] 该模板类中重载了 [] 运算符,其功能是可以向访问数组中元素那样,只要给定某个键值对的键 key,就可以获取该键对应的值。注意,如果当前容器中没有以 key 为键的键值对,则其会使用该键向当前容器中插入一个新键值对
at(key) 返回容器中存储的键 key 对应的值,如果 key 不存在,则会抛出 out_of_range 异常
find(key) 查找以 key 为键的键值对,如果找到,则返回一个指向该键值对的正向迭代器;反之,则返回一个指向容器中最后一个键值对之后位置的迭代器(如果 end() 方法返回的迭代器)
count(key) 在容器中查找以 key 键的键值对的个数
equal_range(key) 返回一个 pair 对象,其包含 2 个迭代器,用于表明当前容器中键为 key 的键值对所在的范围
emplace() 向容器中添加新键值对,效率比 insert() 方法高
emplace_hint() 向容器中添加新键值对,效率比 insert() 方法高
insert() 向容器中添加新键值对
erase() 删除指定键值对
clear() 清空容器,即删除容器中存储的所有键值对
swap() 交换 2 个 unordered_map 容器存储的键值对,前提是必须保证这 2 个容器的类型完全相等
bucket_count() 返回当前容器底层存储键值对时,使用桶(一个线性链表代表一个桶)的数量
max_bucket_count() 返回当前系统中,unordered_map 容器底层最多可以使用多少桶
bucket_size(n) 返回第 n 个桶中存储键值对的数量
bucket(key) 返回以 key 为键的键值对所在桶的编号
load_factor() 返回 unordered_map 容器中当前的负载因子。负载因子,指的是的当前容器中存储键值对的数量(size())和使用桶数(bucket_count())的比值,即 load_factor() = size() / bucket_count()**
max_load_factor() 返回或者设置当前 unordered_map 容器的负载因子
rehash(n) 将当前容器底层使用桶的数量设置为 n
reserve() 将存储桶的数量(也就是 bucket_count() 方法的返回值)设置为至少容纳count个元(不超过最大负载因子)所需的数量,并重新整理容器
hash_function() 返回当前容器使用的哈希函数对象

unordered_map 容器获取元素

  1. 获取键对应的值和 = 新增键值
cpp 复制代码
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main()
{
    // 创建 umap 容器
    unordered_map<string, string> umap{
        {"Python教程","http://c.biancheng.net/python/"},
        {"Java教程","http://c.biancheng.net/java/"},
        {"Linux教程","http://c.biancheng.net/linux/"} };
    // 使用 [] 获取 "Java教程" 对应的值
    string str1 = umap["Java教程"];
    string str2 = umap["JavaScript教程"];
    // 使用 at() 函数获取指定键对应的值,如果查找失败直接抛出 `out_of_range` 异常
    string str3 = umap.at("Python教程");
    cout << str1 << endl;
    cout << str2 << endl;
    cout << str3 << endl;
    // 使用 [] 和 = 新增 "C教程" 键及其对应的值
    umap["C教程"] = "http://c.biancheng.net/c/";
    return 0;
}

运行结果:

txt 复制代码
http://c.biancheng.net/java/
http://c.biancheng.net/python/

注意:

  • 使用 [] 获取键 A 的值如果查找失败,unordered_map 容器会自动添加该键 A 且设值为空字符串
  • 使用 at() 函数获取值如果查找失败直接抛出 out_of_range 异常
  1. find() 函数查找是否包含指定键

通过 find() 方法得到的是一个正向迭代器,该迭代器的指向分以下 2 种情况:

  1. 当 find() 方法成功找到 以指定元素作为键的键值对时,其返回的迭代器指向该键值对
  2. 当 find() 方法查找失败时,其返回的迭代器和 end() 方法返回的迭代器一样,指向容器中最后一个键值对之后的位置,根据这个表示找不到
cpp 复制代码
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;

int main()
{
    // 创建 umap 容器
    unordered_map<string, string> umap{
        {"Python教程","http://c.biancheng.net/python/"},
        {"Java教程","http://c.biancheng.net/java/"},
        {"Linux教程","http://c.biancheng.net/linux/"} };
    // 查找成功
    unordered_map<string, string>::iterator iter = umap.find("Python教程");
    cout << iter->first << " " << iter->second << endl;
    // 查找失败
    unordered_map<string, string>::iterator iter2 = umap.find("GO教程");
    if (iter2 == umap.end()) {
        cout << "当前容器中没有以"GO教程"为键的键值对";
    }
    return 0;
}

执行结果为:

txt 复制代码
Python教程 http://c.biancheng.net/python/  
当前容器中没有以"GO教程"为键的键值对

另外insert() 方法还支持将某一个 unordered_map 容器中指定区域内的所有键值对,复制到另一个 unordered_map 容器中,其语法格式如下:

cpp 复制代码
insert(first, last);

其中 first 和 last 都为迭代器,[first, last)表示复制其它 unordered_map 容器中键值对的区域

举个例子:

cpp 复制代码
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main()
{
    // 创建并初始化 umap 容器
    unordered_map<string, string> umap{ {"STL教程","http://c.biancheng.net/stl/"},
    {"Python教程","http://c.biancheng.net/python/"},
    {"Java教程","http://c.biancheng.net/java/"} };
    // 创建一个空的 unordered_map 容器
    unordered_map<string, string> otherumap;
    // 指定要拷贝 umap 容器中键值对的范围
    unordered_map<string, string>::iterator first = ++umap.begin();
    unordered_map<string, string>::iterator last = umap.end();
    // 将指定 umap 容器中 [first,last) 区域内的键值对复制给 otherumap 容器
    otherumap.insert(first, last);
    // 遍历 otherumap 容器中存储的键值对
    for (auto iter = otherumap.begin(); iter != otherumap.end(); ++iter){
        cout << iter->first << " " << iter->second << endl;
    }
    return 0;
}

程序输出结果为:

txt 复制代码
Python教程 http://c.biancheng.net/python/  
Java教程 http://c.biancheng.net/java/

unordered_map 容器添加元素

insert() 方法

【语法】

cpp 复制代码
umap.insert( {key, value} ); // 添加单个键值对
umap.insert({ {key1, value1}, {key2, value2}, ... }); // 数组形式添加多个键值对

【返回值】

cpp 复制代码
// 构建要添加的键值对
pair<string, string>mypair("STL教程", "http://c.biancheng.net/stl/");
// 创建接收 insert() 方法返回值的 pair 类型变量
pair<unordered_map<string, string>::iterator, bool> ret;
ret = umap.insert(mypair);

返回值为 pair 类型值,内部包含一个迭代器一个 bool 变量

  • 当 insert() 将 val 成功添加到容器中时,返回的迭代器指向新添加的键值对 ,bool 值为 True
  • 当 insert() 添加键值对失败时,意味着当前容器中本就存储有和要添加键值对的键相等的键值对 ,这种情况下,返回的迭代器将指向这个导致插入操作失败的迭代器,bool 值为 False

【使用示例】

cpp 复制代码
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main()
{
    //创建空 umap 容器
    unordered_map<string, string> umap;
    //构建要添加的键值对
    std::pair<string, string>mypair("STL教程", "http://c.biancheng.net/stl/");
    //创建接收 insert() 方法返回值的pair类型变量
    std::pair<unordered_map<string, string>::iterator, bool> ret;
    //调用 insert() 方法的第一种语法格式
    ret = umap.insert(mypair);
    cout << "bool = " << ret.second << endl;
    cout << "iter -> " << ret.first->first <<" " << ret.first->second << endl;
   
    //调用 insert() 方法的第二种语法格式
    ret = umap.insert(std::make_pair("Python教程","http://c.biancheng.net/python/"));
    cout << "bool = " << ret.second << endl;
    cout << "iter -> " << ret.first->first << " " << ret.first->second << endl;
    return 0;
}

执行结果为:

txt 复制代码
bool = 1  
iter -> STL教程 http://c.biancheng.net/stl/  
bool = 1  
iter -> Python教程 http://c.biancheng.net/python/

emplace() 方法

【语法】

cpp 复制代码
umap.emplace(key, value)

【返回值】

该方法的返回值为 pair 类型值 ,其包含一个迭代器一个 bool 类型值

  • 当 emplace() 成功添加新键值对时,返回的迭代器指向新添加的键值对 ,bool 值为 True
  • 当 emplace() 添加新键值对失败时,说明容器中本就包含 一个键相等的键值对,此时返回的迭代器指向的就是容器中键相同 的这个键值对,bool 值为 False

【使用示例】

cpp 复制代码
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main()
{
    // 创建 umap 容器
    unordered_map<string, string> umap;
    // 定义一个接受 emplace() 方法的 pair 类型变量
    pair<unordered_map<string, string>::iterator, bool> ret1;
    pair<unordered_map<string, string>::iterator, bool> ret2;
    // 调用 emplace() 方法
    ret1 = umap.emplace("STL教程", "http://c.biancheng.net/stl/");
    // 输出 ret1 中包含的 2 个元素的值
    cout << "iter -> " << ret1.first->first << " " << ret1.first->second << endl;
    cout << "bool = " << ret1.second << endl;
    // 再次调用 emplace() 方法
    ret2 = umap.emplace("STL教程", "http://c.biancheng.net/stl/");
    // 输出 ret2 中包含的 2 个元素的值
    cout << "iter -> " << ret2.first->first << " " << ret2.first->second << endl;
    cout << "bool = " << ret2.second << endl;
    return 0;
}

执行结果为:

txt 复制代码
iter -> STL教程 http://c.biancheng.net/stl/
bool = 1
iter -> STL教程 http://c.biancheng.net/stl/
bool = 0

emplace_hint() 方法

【语法】

cpp 复制代码
umap.emplace_hint(position, key, value)

emplace_hint() 方法还需要传递一个迭代器作为第一个参数,该迭代器表明将新键值对添加到容器中的位置。需要注意的是,新键值对添加到容器中的位置,并不是此迭代器说了算,最终仍取决于该键值对的键的值

可以这样理解,emplace_hint() 方法中传入的迭代器,仅是给 unordered_map 容器提供一个建议,并不一定会被容器采纳。

【返回值】

emplace_hint() 方法的返回值仅是一个迭代器 ,而不再是 pair 类型变量。当该方法将新键值对成功添加到容器中时,返回的迭代器指向新添加的键值对 ;反之,如果添加失败,该迭代器指向的是容器中和要添加键值对键相同的那个键值对

【使用示例】

cpp 复制代码
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main()
{
    // 创建 umap 容器
    unordered_map<string, string> umap;
    // 定义一个接受 emplace_hint() 方法的迭代器
    unordered_map<string,string>::iterator iter;
    // 调用 empalce_hint() 方法
    iter = umap.emplace_hint(umap.begin(),"STL教程", "http://c.biancheng.net/stl/");
    // 输出 emplace_hint() 返回迭代器 iter 指向的键值对的内容
    cout << "iter ->" << iter->first << " " << iter->second << endl;
    return 0;
}

程序执行结果为:

txt 复制代码
iter ->STL教程 http://c.biancheng.net/stl/

unordered_map 容器删除元素

erase() 方法

【语法】

cpp 复制代码
umap.erase(key)

【返回值】 返回一个整数 ,其表示成功删除键值对的数量

【使用示例】

cpp 复制代码
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main()
{
    // 创建 umap 容器
    unordered_map<string, string> umap{
        {"STL教程", "http://c.biancheng.net/stl/"},
        {"Python教程", "http://c.biancheng.net/python/"},
        {"Java教程", "http://c.biancheng.net/java/"} }; 
    // 输出 umap 容器中存储的键值对
    for (auto iter = umap.begin(); iter != umap.end(); ++iter) {
        cout << iter->first << " " << iter->second << endl;
    }
    int delNum = umap.erase("Python教程");
    cout << "delNum = " << delNum << endl;
    // 再次输出 umap 容器中存储的键值对
    for (auto iter = umap.begin(); iter != umap.end(); ++iter) {
        cout << iter->first << " " << iter->second << endl;
    }
    return 0;
}

程序执行结果为:

txt 复制代码
STL教程 http://c.biancheng.net/stl/  
Python教程 http://c.biancheng.net/python/  
Java教程 http://c.biancheng.net/java/  
delNum = 1  
STL教程 http://c.biancheng.net/stl/  
Java教程 http://c.biancheng.net/java/

clear() 方法

通过调用 clear() 方法,可以把包含多个键值对的 umap 容器,变成了空容器

unordered_set 容器

unordered_set 和 unordered_map 容器的区别是该容器存储的全部都是键 key 和值 value 相等 的键值对,正因为它们相等,因此只存储 value 即可

其余地方相似

cpp 复制代码
#include <iostream>
#include <string>
#include <unordered_set>
using namespace std;

int main()
{
    //创建一个空的unordered_set容器
    std::unordered_set<std::string> uset;
    //给 uset 容器添加数据
    uset.emplace("http://c.biancheng.net/java/");
    uset.emplace("http://c.biancheng.net/c/");
    uset.emplace("http://c.biancheng.net/python/");
    //查看当前 uset 容器存储元素的个数
    cout << "uset size = " << uset.size() << endl;
    //遍历输出 uset 容器存储的所有元素
    for (auto iter = uset.begin(); iter != uset.end(); ++iter) {
        cout << *iter << endl;
    }
    return 0;
}

unordered_multimap 容器

unordered_multimap 容器 和 unordered_map 容器的区别在于:unordered_multimap 容器可以同时存储多个相同键的键值对

其余地方相似

unordered_multiset 容器

unordered_multiset 容器 和 unordered_set 容器的区别在于:unordered_multiset 容器可以同时存储多个相同键的键值对

其余地方相似

有序关联式容器

和序列式容器的最大区别:有序关联式容器的元素是一个一个的"键值对 "(<key,value>
和无序关联式容器的最大区别:有序关联式容器的元素默认会根据各元素的键值的大小升序排序

关联式容器种类包含以下 4 中

关联式容器名称 特点
map 定义在 <map> 头文件中,使用该容器存储的数据,其各个元素的键必须不能重复 ,该容器会根据各元素键的大小,默认进行升序排序(调用 std::less<T>
set 定义在 <set> 头文件中,使用该容器存储的数据,各个元素键和值完全相同 ,且各个元素的值不能重复 。该容器会自动根据各个元素的键的大小进行升序排序(调用 std::less<T>
multimap 定义在 <map> 头文件中,和 map 容器唯一的不同在于,multimap 容器中存储元素的键可以重复
multiset 定义在 <set> 头文件中,和 set 容器唯一的不同在于,multiset 容器中存储元素的值可以重复

map 容器

创建 map 容器

引入头文件和规定命名空间

cpp 复制代码
#include <map>
using namespace std;

语法:

cpp 复制代码
map<key的类型, value的类型, 排序规则>myMap;

示例:

cpp 复制代码
map<string, int>myMap; // 空 map 容器
map<string, int, greater<string>>myMap; // 指定按 key 值字典降序排列的空 map 容器
map<string, int>myMap{ {"C语言教程",10},{"STL教程",20} }; // 创建的同时初始化

map 容器迭代器

成员方法 功能
begin() 返回指向容器中第一个(注意,是已排好序的第一个)键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
end() 返回指向容器最后一个元素(注意,是已排好序的最后一个)所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
rbegin() 返回指向最后一个(注意,是已排好序的最后一个)元素的反向双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。
rend() 返回指向第一个(注意,是已排好序的第一个)元素所在位置前一个位置的反向双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。
find(key) 在 map 容器中查找键为 key 的键值对,如果成功找到,则返回指向该键值对的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
lower_bound(key) 返回一个指向当前 map 容器中第一个大于或等于 key 的键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
upper_bound(key) 返回一个指向当前 map 容器中第一个大于 key 的键值对的迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
equal_range(key) 该方法返回一个 pair 对象(包含 2 个双向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等价,pair.second 和 upper_bound() 方法的返回值等价。也就是说,该方法将返回一个范围,该范围中包含的键为 key 的键值对(map 容器键值对唯一,因此该范围最多包含一个键值对)。

begin()、end() 方法

cpp 复制代码
#include <iostream>
#include <map>      // pair
#include <string>       // string
using namespace std;

int main() {
    //创建并初始化 map 容器
    std::map<std::string, std::string>myMap{ {"STL教程","http://c.biancheng.net/stl/"},{"C语言教程","http://c.biancheng.net/c/"} };

    //调用 begin()/end() 组合,遍历 map 容器
    for (auto iter = myMap.begin(); iter != myMap.end(); ++iter) {
        cout << iter->first << " " << iter->second << endl;
    }
    return 0;
}

find() 方法

cpp 复制代码
#include <iostream>
#include <map>      // pair
#include <string>       // string
using namespace std;

int main() {
    //创建并初始化 map 容器
    std::map<std::string, std::string>myMap{ {"STL教程","http://c.biancheng.net/stl/"},
                                             {"C语言教程","http://c.biancheng.net/c/"},
                                             {"Java教程","http://c.biancheng.net/java/"} };
    //查找键为 "Java教程" 的键值对
    auto iter = myMap.find("Java教程");
    //从 iter 开始,遍历 map 容器
    for (; iter != myMap.end(); ++iter) {
        cout << iter->first << " " << iter->second << endl;
    }
    return 0;
}

map 容器其他成员函数 ☆

成员方法 功能
empty() 若容器为空,则返回 true;否则 false。
size() 返回当前 map 容器中存有键值对的个数。
max_size() 返回 map 容器所能容纳键值对的最大个数,不同的操作系统,其返回值亦不相同。
operator[] map容器重载了 [] 运算符,只要知道 map 容器中某个键值对的键的值,就可以向获取数组中元素那样,通过键直接获取对应的值。
at(key) 找到 map 容器中 key 键对应的值,如果找不到,该函数会引发 out_of_range 异常。
insert() 向 map 容器中插入键值对。
erase() 删除 map 容器指定位置、指定键(key)值或者指定区域内的键值对。后续章节还会对该方法做重点讲解。
swap() 交换 2 个 map 容器中存储的键值对,这意味着,操作的 2 个键值对的类型必须相同。
clear() 清空 map 容器中所有的键值对,即使 map 容器的 size() 为 0。
emplace() 在当前 map 容器中的指定位置处构造新键值对。其效果和插入键值对一样,但效率更高。
emplace_hint() 在本质上和 emplace() 在 map 容器中构造新键值对的方式是一样的,不同之处在于,使用者必须为该方法提供一个指示键值对生成位置的迭代器,并作为该方法的第一个参数。
count(key) 在当前 map 容器中,查找键为 key 的键值对的个数并返回。注意,由于 map 容器中各键值对的键的值是唯一的,因此该函数的返回值最大为 1。

成员函数的使用示例和 unordered_map 容器的使用相似

map 容器的有序性

  1. 默认按照字典升序排序
cpp 复制代码
#include <map>
#include <string>
#include <iostream>
using namespace std;

int main()
{
  map<string, int> name_score_map;
  name_score_map["LiMin"] = 90;
  name_score_map["ZiLinMi"] = 79;
  name_score_map["BoB"] = 92;
  name_score_map["Bing"] = 99;
  name_score_map["Albert"] = 86;
  for (map<string, int>::iterator iter = name_score_map.begin(); iter != name_score_map.end(); ++iter)
  {
    cout << iter->first << " => " << iter->second << '\n';
  }
  return 0;
}

由于 map 容器在存储元素时,会根据各个元素键的大小 自动调整元素的顺序(默认按照字典升序排序),因此该容器最终存储的元素顺序为:

txt 复制代码
Albert => 86
Bing => 99
BoB => 92
LiMin => 90
ZiLinMi => 79
  1. 指定按照字典降序排列

把 map 容器的声明修改为:(加上 greater<string>

cpp 复制代码
map<string, int, greater<string>> name_score_map;

即可 该容器最终存储的元素顺序为:

txt 复制代码
ZiLinMi => 79
LiMin => 90
BoB => 92
Bing => 99
Albert => 86
  1. 自定义排序规则
cpp 复制代码
#include <iostream>
#include <map>

// 自定义比较函数对象
struct CustomCompare {
    bool operator()(const std::string& a, const std::string& b) const {
        // 按字符串长度排序,如果长度相同则按字典序排序
        if (a.length() == b.length()) {
            return a < b;
        } else {
            return a.length() < b.length();
        }
    }
};

int main() {
    std::map<std::string, int, CustomCompare> word_count = {
        {"apple", 3},
        {"banana", 2},
        {"orange", 5},
        {"kiwi", 1}
    };
    for (const auto& pair : word_count) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
    return 0;
}

最终元素顺序为:

txt 复制代码
kiwi: 1
apple: 3
banana: 2
orange: 5

set 容器

set 容器和 map 容器的区别在于:sety 容器要求键 key 和值 value 必须相等,也就是说只用来存储值而已

其余地方相似

cpp 复制代码
#include <iostream>
#include <set>
#include <string>
using namespace std;

int main()
{
    //创建空set容器
    std::set<std::string> myset;
    //空set容器不存储任何元素
    cout << "1、myset size = " << myset.size() << endl;
    //向myset容器中插入新元素
    myset.insert("http://c.biancheng.net/java/");
    myset.insert("http://c.biancheng.net/stl/");
    myset.insert("http://c.biancheng.net/python/");
    cout << "2、myset size = " << myset.size() << endl;
    //利用双向迭代器,遍历myset
    for (auto iter = myset.begin(); iter != myset.end(); ++iter) {
        cout << *iter << endl;
    }
    return 0;
}

multimap 容器

multimap 容器 和 map 容器的区别在于:multimap 容器可以同时存储多个相同键的键值对

其余地方相似

multiset 容器

multiset 容器 和 set 容器的区别在于:multiset 容器可以同时存储多个相同键的键值对

其余地方相似

相关推荐
捕鲸叉10 分钟前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer15 分钟前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq17 分钟前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
青花瓷2 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
幺零九零零3 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
捕鲸叉3 小时前
MVC(Model-View-Controller)模式概述
开发语言·c++·设计模式
Dola_Pan4 小时前
C++算法和竞赛:哈希算法、动态规划DP算法、贪心算法、博弈算法
c++·算法·哈希算法
yanlou2334 小时前
KMP算法,next数组详解(c++)
开发语言·c++·kmp算法
小林熬夜学编程4 小时前
【Linux系统编程】第四十一弹---线程深度解析:从地址空间到多线程实践
linux·c语言·开发语言·c++·算法
阿洵Rain5 小时前
【C++】哈希
数据结构·c++·算法·list·哈希算法