标准模板库(STL)概述:六大组件与容器分类
STL,全称 Standard Template Library,中文叫标准模板库。说实话,我刚学 C++ 那会儿,觉得 STL 就是个黑盒子——用起来爽,但不知道里面怎么运作的。后来做项目多了,才慢慢摸清它的门道。
STL 不是 C++ 语言的一部分,而是 C++ 标准库的核心。它提供了六大组件,彼此配合,构成了一个强大的泛型编程框架。我个人习惯把这六大组件比作一个工厂:容器是仓库,迭代器是传送带,算法是加工机器,仿函数是加工指令,适配器是转换接口,分配器是物料管理员。
六大组件概览
先看一张图,把整体结构理清楚:
容器(Container)
容器就是用来存放数据的东西。说白了,就是各种数据结构的封装。STL 把容器分成两大类:序列式容器和关联式容器。
序列式容器
序列式容器,元素按插入顺序排列。你放进去什么顺序,取出来就是什么顺序。我最早接触的就是 vector,当时觉得这玩意儿比 C 数组好用太多了。
| 容器 | 特点 | 适用场景 |
|---|---|---|
| vector | 动态数组,连续内存,随机访问 O(1) | 频繁随机访问,尾部增删 |
| deque | 双端队列,分段连续,头尾增删 O(1) | 需要头尾操作 |
| list | 双向链表,非连续内存,插入删除 O(1) | 频繁中间插入删除 |
| forward_list | 单向链表,更省内存 | 只需单向遍历 |
| array | 固定大小数组,C++11 引入 | 替代 C 风格数组 |
关联式容器
关联式容器,元素按 key 排序或哈希。你放进去的顺序不重要,取的时候按 key 找。这类容器底层通常是红黑树或哈希表。
| 容器 | 底层结构 | 特点 |
|---|---|---|
| set / multiset | 红黑树 | 自动排序,元素唯一(set)或可重复(multiset) |
| map / multimap | 红黑树 | 键值对存储,按键排序 |
| unordered_set / unordered_multiset | 哈希表 | 无序,查找 O(1) 平均 |
| unordered_map / unordered_multimap | 哈希表 | 无序键值对,查找 O(1) 平均 |
迭代器(Iterator)
迭代器是容器和算法之间的桥梁。它把容器的遍历操作抽象成统一的接口。你想想看,如果没有迭代器,每种容器都得写一套自己的遍历代码,那多麻烦。
迭代器按能力分为五类:
- 输入迭代器:只能读,单向移动。比如从键盘读取数据。
- 输出迭代器:只能写,单向移动。比如写入文件。
- 前向迭代器:可读写,单向移动。比如 forward_list 的迭代器。
- 双向迭代器:可读写,双向移动。比如 list、set、map 的迭代器。
- 随机访问迭代器:可读写,支持跳跃访问。比如 vector、deque、array 的迭代器。
// 一个简单的例子:用迭代器遍历 vector
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用迭代器遍历
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
// 输出:1 2 3 4 5
return 0;
}
std::sort 可以排序 vector、deque、array,只要它们提供随机访问迭代器。
算法(Algorithm)
STL 提供了上百种算法,涵盖排序、查找、变换、数值计算等。算法不直接操作容器,而是通过迭代器操作。这就是泛型编程的精髓——算法只依赖迭代器的能力,不依赖容器的具体类型。
常用的算法有:
std::sort:排序,需要随机访问迭代器std::find:线性查找,输入迭代器即可std::binary_search:二分查找,需要有序序列std::for_each:对每个元素执行操作std::transform:对每个元素进行变换std::copy:复制元素
// 算法示例:排序和查找
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> data = {5, 2, 8, 1, 9};
std::sort(data.begin(), data.end()); // 排序
auto it = std::find(data.begin(), data.end(), 8);
if (it != data.end()) {
std::cout << "找到了: " << *it << std::endl;
}
return 0;
}
仿函数(Functor)
仿函数,说白了就是重载了 operator() 的类对象。它可以像函数一样被调用,但比普通函数灵活——可以保存状态,可以传递参数。
我刚开始觉得仿函数有点多余,直接用函数指针不就行了?后来在项目中需要给 std::sort 传一个带状态的比较器,函数指针搞不定,仿函数就派上用场了。
// 仿函数示例:自定义比较器
struct Descending {
bool operator()(int a, int b) const {
return a > b; // 降序排列
}
};
std::vector<int> vec = {3, 1, 4, 1, 5};
std::sort(vec.begin(), vec.end(), Descending());
// 结果:5, 4, 3, 1, 1
适配器(Adapter)
适配器用来转换接口。STL 提供了三种容器适配器:
- stack:栈,后进先出。底层默认用 deque。
- queue:队列,先进先出。底层默认用 deque。
- priority_queue:优先队列,最大元素先出。底层默认用 vector。
适配器不提供迭代器,也不支持随机访问。它们只暴露特定的操作接口。说白了,就是给底层容器套了一层限制,让你只能用特定的方式操作数据。
分配器(Allocator)
分配器负责内存的分配和释放。默认情况下,STL 容器使用 std::allocator,它调用 new 和 delete。但在某些场景下,你可能需要自定义分配器,比如:
- 使用内存池,减少频繁分配的开销
- 在共享内存中分配数据
- 对齐特殊类型的内存
说实话,日常开发中很少需要自己写分配器。但理解它的存在很重要——它让 STL 容器可以适应各种内存管理需求。我曾经在一个嵌入式项目里用过自定义分配器,把容器的内存分配从堆改到了静态内存池,解决了内存碎片问题。
// 分配器接口的核心部分(简化)
template<typename T>
class MyAllocator {
public:
T* allocate(size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, size_t) {
::operator delete(p);
}
// ... 其他必要接口
};
六大组件如何协作
最后,用一个实际例子看看它们怎么配合:
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main() {
// 容器:vector
std::vector<int> nums = {4, 2, 7, 1, 9, 3};
// 仿函数:greater<int> 是 STL 提供的仿函数
// 适配器:没有显式使用,但 sort 内部可能使用分配器
std::sort(nums.begin(), nums.end(), std::greater<int>());
// 迭代器:begin() 和 end() 返回迭代器
// 算法:for_each
std::for_each(nums.begin(), nums.end(), [](int n) {
std::cout << n << " ";
});
// 输出:9 7 4 3 2 1
return 0;
}
看到了吗?六大组件各司其职,组合起来就能写出简洁、高效、可复用的代码。这就是 STL 的魅力所在。