一文看懂C++标准库
标准模板库(standard template library,STL)是基于泛型编程的,即C++官方通过函数模板和类模板提前写好大量通用的数据类型和算法,并内置在支持C++的编译器中,以方便程序员开发软件时快速调用,而不必关注具体的数据类型。STL是容器(即类模板)的集合,这些容器在算法库的支持下使程序开发变得简单、高效。STL无须额外安装,使用起来非常方便。
以在C++中定义一个数组为例,如果使用传统方法,需要先声明数组的大小,然后定义数组:
1
int arr[10];
这种定义数组的方法需要事先知道数组的长度,因为n必须为常量。如果无法确定数组长度,就需要将数组长度设为可能的最大值,这会导致存储空间的极大浪费。为了解决这个问题,还可以采用在堆空间中动态申请内存的方法:
1
int *arr = new int[10]; // 在堆空间中申请10个int类型的内存
这种方法虽然解决了数组长度不确定的问题,但如果程序执行过程中出现空间不足的情况,需要加大存储空间时,就需要进行如下4步操作。
1
2
3
4
int *temp = new int[20]; // 重新申请20个int类型的内存
memcpy(temp, arr, 10 * sizeof(int)); // 将arr中的数据复制到temp中
delete[] arr; // 释放arr的内存
arr = temp; // 将temp的地址赋值给arr
如果使用STL,上面的操作会简单很多,只需要定义一个可以动态存储数据的容器vector,不用再关心内存操作细节。
1
2
3
4
5
6
7
8
vector<int> arr;
for (int i = 0; i < 10; i++) {
arr.push_back(i);
}
arr.resize(20); // 重新申请20个int类型的内存
arr[19] = 19;
arr.pop_back(); // 删除最后一个元素
arr.resize(10, -1); // 将arr的大小调整为10,并将所有元素设置为-1
常见的数据结构
- 数组:最典型的线性结构,其基本特点是数据元素是有限且有序的。数组会为每个存储的元素分配一个下标(索引),此下标从0开始,连续自增,通过数组名和下标可快速访问所有的元素。数组中,各元素在内存中是连续存放的,因此可通过数组名(或指针)和偏移量快速访问任意一个元素,但添加和删除数据却比较麻烦和耗时。因此,数组结构适用于需要频繁查询数据,但对存储空间要求不大,数据增、删操作较少的情况。
- 栈:一种特殊的线性结构,数据的存储和访问仅能在线性表的一端(即栈顶)进行,栈底则不允许操作。栈的结构特点决定了其数据进出顺序为后进先出(LIFO)。栈不擅长在随机位置插入和删除对象(需要借助其他方法才能实现),比较耗时。若存储的对象超过自身容量,会发生栈溢出。
- 队列:一种特殊的线性结构,数据的存储和访问仅能在线性表的一端(即队尾)进行,另一端(即队头)则不允许操作。队列的结构特点决定了其数据进出顺序为先进先出(FIFO)。
- 链表:一种非连续、非顺序的线性结构,由一系列结点组成。第一个元素的位置由头指针(head)给出,其后每个结点中都含有数据部分和指针部分,通过指针可访问下一个元素,链表尾部结点的指针指向空(NULL)。链表的存储空间是随机、分散的,无法通过索引快速定位元素,只能通过遍历自身找到元素,因此查找元素效率较低。
- 树:是一种由多个结点组成的具有层次关系的集合,根结点在上,叶结点在下,所有结点都可用来存储数据。二叉树中,每个结点最多有两个子结点,左子树和右子树是有顺序的,次序不能颠倒。使用二叉树添加和删除数据都非常快,查找数据方面也有很多优化的算法。所以,二叉树既有链表的好处,也有数组的好处,是两者的优化方案,在处理大批量的动态数据方面非常有用。
- 图:一种复杂的非线性数据结构,可表示多对多的数据关系。图由顶点和边组成,每条边连接一对顶点。根据边是否具有方向,可将图分为有向图和无向图。
- 哈希表:一种先建立映射关系,然后通过关键字访问其对应值的数据结构。哈希表中数据以键值对的形式存储。通过哈希函数,将键(key)映射为一个固定大小的数组索引,将值(value)存储在以该数字为下标的数组空间里。这种存储方式可以充分利用数组的优势来查找元素。散列表的关键是哈希函数的设计。散列表支持高效的数据插入、查找和删除操作。
STL容器与迭代器
C++ STL有六大组件:容器、算法、迭代器、函数对象、适配器和记忆体配置器。其中最重要的是前3个组件:容器、算法和迭代器。
- 容器(container):用于存放数据的类模板,STL就是各类容器的集合。
- 算法(algorithm):对容器中元素进行操作的函数模板。STL提供了大量算法,可对数据进行查找、排序、遍历、复制、替换、删除、去重等。这些算法具有高度的通用性和可扩展性,可适用于不同类型的容器和元素。
- 迭代器(iterator):遍历容器中元素的类模板。可将迭代器看作是指针,它可以指向容器中的元素,并支持自增、自减、解除引用等操作。
容器
容器就是存放数据的类模板。STL中的容器分为两类,即序列式容器和关联式容器。
序列式容器
序列式容器采用线性结构存储数据,其中可存储基本数据类型(如int、double、float、string等)或结构体类型的元素。序列式容器不会自动对存储的元素按值进行排序,也就是说其元素大小是无序的。将元素插入序列式容器中时,指定什么位置,元素就会位于什么位置。
序列式容器包括:
- array:数组。以类模板
array<T,N>
的形式定义,是一个内存长度固定、由N个T类型元素组成的容器。array对象一旦建立,其在内存中的长度就是固定不变的,不能增加和删除元素,只能改变某个元素的值。array对象使用的是一段连续的内存空间,因此可通过索引快速访问其中的任意一个元素,访问效率非常高。 - vector:向量,又称为动态连续数组。以类模板
vector<T>
的形式定义,是一个内存长度可变、由多个T类型元素组成的容器。vector对象中,在尾部插入和删除元素的效率非常高,在其他位置插入和删除元素的效率较低。插入和删除元素时,会动态调整占用的内存空间。 - deque:双端队列。以类模板
deque<T>
的形式定义,是一个内存长度可变、由多个T类型元素组成的容器。deque对象中,在头部和尾部插入和删除元素的效率都非常高,在其他位置插入和删除元素的效率较低。插入和删除元素时,会动态调整占用的内存空间。 - list:双向链表。以类模板
list<T>
的形式定义,是一个内存长度可变、由多个T类型元素组成的容器,元素的存储位置是随机、分散的(并非连续的内存空间),前后元素的顺序靠指针来维系。其每个元素都配备了两个指针,分别指向前一个元素和后一个元素,第一个元素的前向指针为null,最后一个元素的后向指针为null。list对象中,在任意位置插入和删除元素的效率都非常高,但访问元素的效率较低。 - forward_list:正向链表,是C++ 11中新增加的容器。以类模板
forward_list<T>
的形式定义,是一个内存长度可变、由多个T类型元素组成的容器,其底层实现和list容器非常接近,但每个元素只配备一个指针,指向其后一个元素(最后一个元素指向null)。同样,forward_list对象中,增加和删除元素效率较高,访问元素效率较低。forward_list容器比list容器更快、更节省内存。
关联式容器
关联式容器在存储元素值时,会为元素额外配备一个键,根据键(key)可快速锁定目标元素。也就是说,关联式容器存储的是表示某种对应关系的键值对(<key,value>),这是和序列式容器最大的不同。
关联式容器没有所谓的头、尾端,只有最大元素和最小元素。插入元素时,可根据其键值大小和容器的排序规则,直接将其放置于合适位置。查找、删除、修改元素时,可根据其键值快速定位到具体元素,执行相应的操作。在数据检索时比序列式容器效率更高。
pair类模板
pair类模板是C++ 11标准库中新增的模板类,,它有两个成员变量first和second,作用是创建“键值对”形式的元素<first, second>。pair类模板定义在头文件utility
中。
1
2
3
4
5
template <class T1, class T2>
struct pair {
T1 first;
T2 second;
};
关联式容器包括:
- map:映射。一般情况下,map容器中存储的键值对都是string类型。map容器存储的键值对,键的值必须唯一,不能重复,也不能被修改。当map容器中存储了多个键值对时,会自动根据键的大小进行排序。默认情况下,排序规则为less
,即根据键做升序排序。也可以设置为greater ,即根据键做降序排序。其中,T为键的数据类型。 - multimap:多重映射,与map容器类似,不同之处在于multimap容器可以同时存储多个键相同的键值对,即键不要求唯一。
- set:集合,是一个内容自动有序且不含重复元素的容器。使用set容器存储的键值对,要求键(key)和值(value)必须相等。 存储数据时,自动根据元素值的大小进行排序,默认为升序排序。存储到set容器中的元素,元素值无法修改。存储的元素必须互不相等。
- multiset:多重集合,和set容器相似,区别在于它可以存储多个值相同的元素。
C++ 11标准库又加入了4种无序关联式容器,即哈希容器。其中的元素是未排序的,元素位置由哈希函数确定。哈希容器包括如下4类。
- unordered_map:哈希映射。
- unordered_multimap:哈希多重映射。
- unordered_set:哈希集合。
- unordered_multiset:哈希多重集合。
容器适配器
容器适配器主要针对序列式容器,它通过定义新的接口,将不适用的容器变为适用的容器。这有点类似于生活中的电源适配器,将不兼容(如不同电压标准)的电源转换为适合的电源。
常见的容器适配器如下:
- stack:栈。
- queue:队列。
- priority_queue:优先级队列。
迭代器
访问容器中的元素需要通过迭代器。迭代器可以指向容器中的某个元素,进而读写该元素的值。从这一点上看,迭代器和指针非常类似。迭代器分为如下5种。
- 输入迭代器:又称为只读迭代器,它从容器中读取元素,一次只能读入一个元素并向前移动。只支持一遍算法,即同一个输入迭代器不能遍历一个序列两次。
- 输出迭代器:又称为只写迭代器,它往容器中写入元素,一次只能写入一个元素并向前移动。同一个输出迭代器不能遍历一个序列两次。
- 正向迭代器:组合了输入迭代器和输出迭代器的功能,且可以多次解析一个迭代器指定位置,可以对一个值进行多次读/写。
- 双向迭代器:组合了正向迭代器的功能,且可通过操作符向后移动位置。
- 随机访问迭代器:组合了双向迭代器的功能,可以向前、向后跳过任意个位置,且可以直接访问容器中任意位置的元素。
不同容器对应的迭代器类型如下表所示:
容器类型 | 对应的迭代器类型 |
---|---|
array | 随机访问迭代器 |
vector | 随机访问迭代器 |
deque | 随机访问迭代器 |
list | 双向迭代器 |
map/multimap | 双向迭代器 |
set/multiset | 双向迭代器 |
forward_list | 正向迭代器 |
unordered_map/unordered_multimap | 正向迭代器 |
unordered_set/unordered_multiset | 正向迭代器 |
stack | 不支持迭代器 |
queue | 不支持迭代器 |
算法
STL中,算法是参数化一个或多个迭代器类型的函数模板,它与具体对象在容器中的位置无关。标准算法包括非修正序列算法、修正序列算法、排序算法和数值算法4类。
非修正序列算法
非修正序列算法是指不修改容器中元素的值,也不会改变元素的次序,如统计元素个数、查找某个元素等。
STL中,常用的非修正序列算法如下所示:
- count(first, last, value):统计容器中等于指定值的元素个数。
- count_if(first, last, func):统计容器中符合条件的元素个数。
- find(first, last, value):查找容器中等于指定值的元素。
- find_if(first, last, func):查找容器中符合条件的元素。
- find_first_of(first, last, first2, last2):查找容器中第一个符合条件的元素。
- find_end_of(first, last, first2, last2):查找容器中最后一个符合条件的元素。
- adjacent_find(first, last):查找容器中相邻且重复的元素。
- search(first, last, first2, last2):查找容器中子序列。
- for_each(first, last, func):遍历容器中的每个元素。
- min_element(first, last):查找容器中最小的元素。
- max_element(first, last):查找容器中最大的元素。
修正序列算法
修正序列算法的有些操作会改变容器的内容。例如,用指定的值填充容器,或把容器的部分内容复制到同一容器的另一部分。
STL中,常用的修正序列算法如下所示:
- fill(first, last, value):用指定值填充容器。
- fill_n(first, n, value):用指定值填充容器中前n个元素。
- copy(first, last, result):把容器中从first到last的元素复制到result中。
- copy_n(first, n, result):把容器中从first开始的n个元素复制到result中。
- copy_backward(first, last, result):把容器中从first到last的元素复制到result中,但复制顺序是从后往前。
- move(first, last, result):把容器中从first到last的元素移动到result中。
- move_backward(first, last, result):把容器中从first到last的元素移动到result中,但移动顺序是从后往前。
- generate(first, last, func):用func函数生成容器中的元素。
- random_shuffle(first, last):随机打乱容器中元素的顺序。
- remove(first, last, value):删除容器中等于指定值的元素。
- replace(first, last, old_value, new_value):把容器中等于old_value的元素替换为new_value。
- rotate(first, middle, last):把容器中从first到middle的元素和从middle到last的元素进行交换。
- reverse(first, last):把容器中从first到last的元素进行反转。
- swap(a, b):交换a和b的值。
- swap_ranges(first1, last1, first2):交换first1到last1和first2开头的元素序列。
- transform(first, last, result, func):对容器中从first到last的元素进行func函数操作,并将结果保存到result中。
- unique(first, last):删除容器中相邻且重复的元素。
排序算法
排序算法用于对容器中的元素进行排序。STL中,常用的排序算法如下所示:
- sort(first, last):对容器中从first到last的元素进行排序,默认为升序。
- stable_sort(first, last):对容器中从first到last的元素进行稳定排序,即值相同的元素位置不变。
- partial_sort(first, middle, last):对容器中从first到last的元素进行部分排序,即只排序出位置middle的元素。
- partial_sort_copy(first, last, result_first, result_last):对容器中从first到last的元素进行部分排序,并将结果保存到result_first到result_last中。
- nth_element(first, nth, last):对容器中从first到last的元素进行排序,并将第nth个元素放在第nth位置。
- is_sorted(first, last):判断容器中从first到last的元素是否已排序。
- is_sorted_until(first, last):判断容器中从first到last的元素是否已排序,并返回第一个未排序的元素位置。
- merge(first1, last1, first2, last2, result):合并两个有序序列,并存储到result中。
数值算法
数值算法用于对容器中的元素进行数值操作。STL中,常用的数值算法如下所示:
- accumulate(first, last, init):累加求和,计算容器中从first到last的元素的和,初始值为init。
- inner_product(first1, last1, first2, init):内积求和,两个容器中从first1到last1和first2到last2的元素两两相乘后再求和,初始值为init。
- partial_sum(first, last, result):部分求和,计算容器中从first到last的元素的和,并存储到result中。
- adjacent_difference(first, last, result):相邻差,容器中从first到last的元素两两求差,并存储到result中。