STL
什么是 STL
STL,中文可译为标准模板库或者泛型库,其包含有大量的模板类和模板函数,是 C++ 提供的一个基础模板的集合,用于完成诸如输入/输出、数学计算等功能。
STL 有什么用
以 C++ 定义数组的操作为例,在 C++ 中如果定义一个数组,可以采用如下方式:
int a[n];
这种定义数组的方法需要事先确定好数组的长度,即 n 必须为常量,这意味着,如果在实际应用中无法确定数组长度,则一般会将数组长度设为可能的最大值,但这极有可能导致存储空间的浪费。
所以除此之外,还可以采用在堆空间中动态申请内存的方法,此时长度可以是变量:
int *p = new int[n];
这种定义方式可根据变量 n 动态申请内存,不会出现存储空间浪费的问题。但是,如果程序执行过程中出现空间不足的情况时,则需要加大存储空间,此时需要进行如下操作:
新申请一个较大的内存空间,即执行int * temp = new int[m];
将原内存空间的数据全部复制到新申请的内存空间中,即执行memecpy(temp, p, sizeof(int)*n);
将原来的堆空间释放,即执行delete [] p; p = temp;
而完成相同的操作,如果采用 STL 标准库,则会简单很多。下面是使用向量模板类 vector
实现以上功能的示例:
// 定义 a 数组,当前数组长度为 0,
// 但和普通数组不同的是,此数组 a 可以根据存储数据的数量【自动变长】
vector 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 命名空间中定义,其中大部分算法都包含在头文件 中,少部分位于头文件 中 |
迭代器 | 在 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,扮演着容器和算法之间的胶合剂 |
函数对象 | 如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数) |
适配器 | 可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器 |
内存分配器 | 为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用 |
C++ 标准库中有这 13 个标准头文件
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 种。
输入迭代器和输出迭代器比较特殊,它们不是把数组或容器当做操作对象,而是把输入流/输出流作为操作对象
我们先主要关注后面三种迭代器:
前向迭代器
假设 p 是一个前向迭代器,则 p 支持 ++p,p++,*p
操作,还可以被复制或赋值,可以用 ==
和 !=
运算符进行比较。此外,两个正向迭代器可以互相赋值。
双向迭代器
双向迭代器具有正向迭代器的全部功能,除此之外,假设 p 是一个双向迭代器,则还可以进行 --p
或者 p--
操作(即一次向后移动一个位置)。
随机访问迭代器
随机访问迭代器具有双向迭代器的全部功能。除此之外,假设 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 容器有以下几种做法。下面的程序中,每个循环演示了一种做法:
#include
// 需要引入 vector 头文件
#include
using namespace std;
int main()
{
vector v{1,2,3,4,5,6,7,8,9,10}; // v 被初始化成有 10 个元素
cout