一、STL 到底是什么?
说实话,我刚学 C++ 那会儿,对 STL 的第一印象就是——这玩意儿也太好用了吧!
STL,全称 Standard Template Library,中文叫标准模板库。它不是 C++ 语言本身的一部分,而是 C++ 标准委员会提供的一套通用库。说白了,就是一群大佬帮你写好了常用的数据结构和算法,你直接拿来用就行。
我在公司做项目时,经常看到新人自己手写链表、自己实现排序。嗯,精神可嘉,但真的没必要。STL 已经帮你搞定了,而且比你写的更稳定、更高效。
STL 的核心思想: 把数据结构和算法分离,通过迭代器作为桥梁,让它们可以灵活组合。
二、STL 六大组件概述
STL 由六大组件组成,我习惯把它们分成三组来理解:
| 组件 | 作用 | 类比 |
|---|---|---|
| 容器(Container) | 存储数据 | 就像各种箱子 |
| 迭代器(Iterator) | 遍历容器中的元素 | 就像箱子的把手 |
| 算法(Algorithm) | 对数据进行操作 | 就像加工工具 |
| 仿函数(Functor) | 让对象像函数一样调用 | 就像可编程的按钮 |
| 适配器(Adapter) | 对组件进行包装 | 就像转换插头 |
| 分配器(Allocator) | 管理内存分配 | 就像仓库管理员 |
你可能会问:六个组件都要学吗?我的建议是——先把前四个搞明白。适配器和分配器属于进阶内容,日常开发中用得不多。
三、容器:你的数据该放哪儿?
容器就是用来存放数据的。STL 提供了两大类容器:序列式容器和关联式容器。
3.1 vector —— 动态数组
vector 是我用得最多的容器,没有之一。它就是一个可以自动扩容的数组。
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec;
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
// 像数组一样访问
std::cout << vec[1] << std::endl; // 输出 20
// 遍历
for (int i = 0; i < vec.size(); ++i) {
std::cout << vec[i] << " ";
}
return 0;
}
我的经验: 如果你不确定用哪种容器,先用 vector。它性能好,内存连续,缓存友好。我在项目中 80% 的场景都用 vector 搞定了。
3.2 list —— 双向链表
list 是双向链表,适合频繁插入和删除的场景。
#include <list>
std::list<int> myList;
myList.push_back(1);
myList.push_front(0); // 头部插入
myList.insert(++myList.begin(), 5); // 在第二个位置插入 5
注意: list 不支持随机访问,你不能写 myList[3]。想访问第 4 个元素?你得从头遍历过去。我曾经在项目里踩过这个坑,当时还纳闷为什么编译报错……
3.3 map —— 键值对字典
map 存储的是键值对,键是唯一的。底层用红黑树实现,查找很快。
#include <map>
#include <string>
std::map<std::string, int> scores;
scores["Alice"] = 95;
scores["Bob"] = 87;
scores["Charlie"] = 92;
// 查找
if (scores.find("Bob") != scores.end()) {
std::cout << "Bob's score: " << scores["Bob"];
}
3.4 set —— 集合
set 就是数学里的集合,元素唯一且自动排序。
#include <set>
std::set<int> s;
s.insert(3);
s.insert(1);
s.insert(2);
s.insert(3); // 重复插入无效
// 输出:1 2 3(自动排序)
for (int x : s) {
std::cout << x << " ";
}
四、迭代器:容器的通用访问方式
迭代器是什么?你可以把它想象成一个指针,但它比指针更聪明。
不同的容器有不同的迭代器类型:
| 迭代器类型 | 支持的操作 | 典型容器 |
|---|---|---|
| 输入迭代器 | 只读,单向移动 | istream_iterator |
| 输出迭代器 | 只写,单向移动 | ostream_iterator |
| 前向迭代器 | 读写,单向移动 | forward_list |
| 双向迭代器 | 读写,双向移动 | list, set, map |
| 随机访问迭代器 | 读写,任意跳转 | vector, deque |
#include <vector>
#include <iostream>
std::vector<int> vec = {10, 20, 30, 40, 50};
// 使用迭代器遍历
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " "; // *it 获取元素
}
// C++11 范围 for 循环(底层也是迭代器)
for (int x : vec) {
std::cout << x << " ";
}
小技巧: 我习惯用 auto 声明迭代器,省得写长长的类型名。比如 auto it = vec.begin(),编译器会自动推导类型。
五、算法:STL 的灵魂
算法是 STL 最强大的部分。你不需要自己写排序、查找,直接调用就行。
5.1 sort —— 排序
#include <algorithm>
#include <vector>
std::vector<int> v = {5, 2, 8, 1, 9};
std::sort(v.begin(), v.end()); // 升序排序
// 降序排序
std::sort(v.begin(), v.end(), std::greater<int>());
5.2 find —— 查找
#include <algorithm>
#include <vector>
std::vector<int> v = {10, 20, 30, 40, 50};
auto it = std::find(v.begin(), v.end(), 30);
if (it != v.end()) {
std::cout << "找到了: " << *it;
} else {
std::cout << "没找到";
}
5.3 for_each —— 遍历处理
#include <algorithm>
#include <vector>
#include <iostream>
std::vector<int> v = {1, 2, 3, 4, 5};
// 打印每个元素
std::for_each(v.begin(), v.end(), [](int x) {
std::cout << x * 2 << " "; // 输出:2 4 6 8 10
});
六、仿函数:让对象像函数一样调用
仿函数,说白了就是重载了 operator() 的类。它为什么有用?因为它可以保存状态。
#include <iostream>
class Adder {
public:
Adder(int n) : num(n) {}
int operator()(int x) {
return x + num;
}
private:
int num;
};
int main() {
Adder add5(5);
std::cout << add5(10); // 输出 15
std::cout << add5(20); // 输出 25
return 0;
}
你看,add5 是一个对象,但它用起来就像函数。而且它内部保存了 num=5,每次调用都基于这个状态。
实际应用: 仿函数经常和算法搭配使用。比如 sort 的第三个参数,你可以传一个仿函数来自定义排序规则。
七、知识体系总览
下面这张图是我自己整理的 STL 核心知识结构,你看一眼就能明白各个组件之间的关系:
八、避坑指南
最后,分享几个我踩过的坑:
- 迭代器失效: 对 vector 进行插入或删除操作后,之前的迭代器可能就失效了。我曾经在循环里删除元素,结果程序直接崩溃。解决办法是用 erase 返回的新迭代器。
- map 的 [] 操作: 用 map[key] 访问不存在的键时,会自动插入一个默认值。如果你只是想查找,用 find 更安全。
- sort 要求随机访问迭代器: list 不能用 sort,得用 list 自己的 sort 成员函数。
我的建议: 刚开始学 STL,先把 vector、map、sort、find 这四个用熟。它们能解决你 90% 的日常需求。其他的,遇到再学也不迟。
好了,STL 入门就讲到这里。记住一句话:STL 不是让你重新发明轮子,而是让你站在巨人的肩膀上写代码。