C++ STL 学习笔记
C++ STL(Standard Template Library)是C++标准库的核心,封装了常用数据结构与算法,凭借"泛型编程"思想实现高复用性,让开发者无需重复造轮子,专注于核心业务逻辑。本文基于STL底层原理、关键特性与实战场景,整理成学习笔记,覆盖容器、迭代器、适配器等核心组件。
一、STL 核心组件概览
STL由五大核心组件构成,组件间相互配合,实现"数据存储-访问-操作"的完整闭环:
| 组件 | 作用 |
|---|---|
| 容器(Containers) | 存储数据的载体,底层封装数组、链表、红黑树、哈希表等数据结构 |
| 迭代器(Iterators) | 连接容器与算法的"桥梁",提供统一的元素访问接口,屏蔽容器底层差异 |
| 算法(Algorithms) | 基于迭代器的通用操作(排序、查找、遍历等),支持对不同容器批量处理 |
| 仿函数(Functors) | 重载()运算符的类/结构体,作为算法参数(如自定义排序规则),封装行为 |
| 适配器(Adapters) | 对现有组件封装改造,改变接口或功能(如stack基于deque实现栈功能) |
二、容器详解:分类、原理与场景
容器是STL的核心,按存储逻辑分为顺序容器 、关联容器 、无序容器三类,底层实现决定其性能特性与适用场景。
(一)容器分类与核心特性总览
| 容器类别 | 底层实现 | 核心特点 | 代表容器 |
|---|---|---|---|
| 顺序容器 | 动态数组/链表 | 元素物理位置与插入顺序一致,线性存储 | vector、list、deque、array、forward_list |
| 关联容器(有序) | 红黑树(自平衡二叉搜索树) | 元素按键有序存储,键唯一/可重复 | map、set、multimap、multiset |
| 无序容器 | 哈希表 | 元素无序存储,基于哈希函数快速访问 | unordered_map、unordered_set、unordered_multimap、unordered_multiset |
(二)顺序容器:线性存储,注重顺序与访问效率
顺序容器维护元素的线性序列,核心差异体现在内存布局与插入/删除效率,详细特性如下:
1. 各顺序容器对比
| 容器 | 底层实现 | 核心特点 | 时间复杂度(插入/删除/查找) | 适用场景 |
|---|---|---|---|---|
| vector | 动态数组 | 连续内存、支持随机访问,尾部插入/删除高效 | 尾部O(1)、中间/头部O(n)、查找O(n) | 随机访问频繁、尾部增删(如图像处理、动态数据存储) |
| list | 双向链表 | 非连续内存、不支持随机访问,任意位置增删高效 | 任意位置O(1)(需迭代器)、查找O(n) | 中间频繁增删(如编辑器撤回栈、日志队列) |
| deque | 分段动态数组 | 双端高效增删,支持随机访问(效率略低于vector) | 头尾O(1)、中间O(n)、查找O(n) | 双端增删(如滑动窗口算法、LRU缓存) |
| array | 固定大小数组 | 栈上分配、连续内存,大小编译时确定 | 访问O(1)、增删不支持(固定大小) | 大小固定、性能敏感(如嵌入式开发、方向向量) |
| forward_list | 单向链表 | 非连续内存、节省空间,仅支持单向遍历 | 头部O(1)、其他位置O(n)、查找O(n) | 内存受限、仅需单向遍历的场景 |
2. 顺序容器关键问题解析
(1)vector 核心原理
- 连续存储保证 :内部通过单一内存块存储元素,维护三个核心变量:
T* elements(内存起始指针)、size(当前元素个数)、capacity(当前内存容量),通过指针运算实现随机访问。 - 扩容机制 :当
size == capacity(空间不足)时,触发扩容:- 申请原容量1.5~2倍的新内存(GCC为2倍,VS为1.5倍);
- 将旧内存中元素复制/移动到新内存(支持移动语义的对象用移动构造,否则用拷贝);
- 释放旧内存。
- 为什么成倍扩容?:避免频繁扩容导致的O(n)时间复杂度,保证增删操作平均O(1);1.5~2倍兼顾内存利用率(3倍及以上易造成内存浪费)。
(2)push_back 与 emplace_back 的区别
两者均用于在容器尾部添加元素,核心差异在于"元素构造方式":
| 特性 | push_back | emplace_back |
|---|---|---|
| 参数类型 | 已构造的元素(左值/右值) | 元素构造函数的参数 |
| 构造方式 | 先创建元素(或移动),再拷贝到容器尾部 | 直接在容器尾部"就地构造"元素 |
| 性能 | 可能产生临时对象,有拷贝/移动开销 | 无临时对象,开销更低(尤其构造复杂对象时) |
| 适用场景 | 添加已存在的对象 | 需传递多个参数构造新对象 |
示例代码:
cpp
// 自定义类
class Person {
public:
Person(int age, string name) : age(age), name(name) {}
private:
int age;
string name;
};
vector<Person> vec;
vec.push_back(Person(20, "Tom")); // 先构造临时对象,再移动/拷贝
vec.emplace_back(20, "Tom"); // 直接在vec尾部构造,无临时对象
(3)vector 与 array 的核心对比
| 特性 | vector(动态数组) | array(固定数组,C++11引入) |
|---|---|---|
| 大小 | 运行时动态可变 | 编译时固定不变 |
| 内存分配 | 堆内存(动态分配) | 栈内存(静态分配) |
| 效率 | 稍慢(动态扩容开销) | 更快(无额外内存管理开销) |
| 适用场景 | 数据量未知、需动态扩容(如读取文件内容) | 大小固定、性能敏感(如嵌入式、算法常量数组) |
| 与C数组对比 | 支持动态扩容、STL算法 | 类型安全、避免指针退化,支持STL算法 |
(三)关联容器:有序存储,注重查找与排序
关联容器基于红黑树实现(自平衡二叉搜索树),元素按键自动排序,查找、插入、删除时间复杂度均为O(logn),核心差异在于存储方式与键的唯一性:
1. 关联容器对比
| 容器 | 存储方式 | 键的特性 | 访问方式 | 适用场景 |
|---|---|---|---|---|
| map | 键值对(key-value) | 键唯一 | 下标访问(operator[])、迭代器 | 字典映射(如配置解析、用户ID-信息映射) |
| set | 仅存储键(元素本身) | 元素唯一 | 仅迭代器访问 | 去重、唯一性判断(如用户ID集合、标签去重) |
| multimap | 键值对(key-value) | 键可重复 | 仅迭代器访问(需遍历相同键元素) | 一对多映射(如学生-课程、员工-任务) |
| multiset | 仅存储键(元素本身) | 元素可重复 | 仅迭代器访问 | 可重复元素的有序存储(如成绩排名、日志级别统计) |
2. 关联容器关键操作:元素查找
map/set 提供四种核心查找方法,适用于不同场景:
| 方法 | 功能描述 | 返回值 |
|---|---|---|
| find(key) | 查找指定键,找到返回对应迭代器,否则返回end() | 指向目标元素的迭代器 |
| count(key) | 返回指定键的元素个数(map/set返回0或1) | size_t类型(0表示未找到) |
| lower_bound(key) | 返回指向"不小于key"的首个元素的迭代器 | 迭代器(有序容器中有效) |
| upper_bound(key) | 返回指向"大于key"的首个元素的迭代器 | 迭代器(有序容器中有效) |
| equal_range(key) | 返回包含"等于key"所有元素的迭代器对([lower, upper)) | pair<iterator, iterator> |
示例代码(map查找):
cpp
map<int, string> myMap = {{1, "one"}, {2, "two"}};
// 1. find方法
auto it = myMap.find(1);
if (it != myMap.end()) cout << "找到:" << it->second << endl;
// 2. count方法
if (myMap.count(2)) cout << "找到key=2" << endl;
// 3. lower_bound/upper_bound
auto lower = myMap.lower_bound(1);
auto upper = myMap.upper_bound(1);
if (lower != upper) cout << "找到:" << lower->second << endl;
(四)无序容器:哈希存储,注重高效访问
无序容器(C++11引入)基于哈希表实现,元素无序存储,平均查找、插入、删除时间复杂度为O(1),核心特性与关联容器对比:
1. 无序容器与关联容器核心差异
| 特性 | 无序容器(unordered_*) | 关联容器(map/set) |
|---|---|---|
| 底层实现 | 哈希表 | 红黑树 |
| 元素顺序 | 无序(取决于哈希函数) | 有序(按键自动排序) |
| 时间复杂度 | 平均O(1),极端O(n)(哈希冲突严重) | 稳定O(logn) |
| 空间开销 | 较高(哈希表需存储桶、链表指针) | 较低(红黑树仅存储节点数据与颜色信息) |
| 适用场景 | 无需排序、追求高效查找(如缓存、计数器) | 需要有序存储、稳定性能(如排行榜、范围查询) |
2. 关键注意点:哈希容器的 rehash
当无序容器的元素个数超过"负载因子"(默认1.0)时,会触发rehash(重建哈希表),此时所有迭代器失效。避免失效的方法:
- 批量插入前调用
reserve(n),预分配足够容量,减少rehash次数; - rehash后需重新获取迭代器,不可使用旧迭代器。
三、迭代器:容器的"通用指针"
(一)迭代器的核心作用
迭代器提供容器无关的统一元素访问接口 ,无论容器底层是数组、链表还是红黑树,都可通过++(递增)、*(解引用)等操作遍历元素,实现"算法与容器解耦"。
(二)迭代器失效:高频踩坑点
迭代器失效指容器修改后(如插入、删除、扩容),原有迭代器不再指向有效元素,继续使用会导致未定义行为(UB)。不同容器的失效规则如下:
1. 各容器迭代器失效规则
| 容器类型 | 失效场景 | 不失效场景 |
|---|---|---|
| vector/string | 扩容(push_back/insert)、中间插入/删除 | 尾部删除(pop_back)、未扩容的尾部插入 |
| deque | 首/尾插入删除(部分实现)、中间插入删除 | - |
| list/forward_list | 仅被删除元素的迭代器失效 | 插入、拼接操作 |
| map/set(红黑树) | 仅被删除节点的迭代器失效 | 插入操作 |
| 无序容器(哈希) | rehash(reserve/插入触发)、被删除元素迭代器 | 未触发rehash的插入操作 |
2. 避免迭代器失效的实战技巧
-
使用 erase 返回值 :C++11起,
erase(it)返回下一个有效迭代器,适用于边遍历边删除:cpp// vector安全删除偶数元素 vector<int> vec = {1,2,3,4}; for (auto it = vec.begin(); it != vec.end(); ) { if (*it % 2 == 0) it = vec.erase(it); // 用返回值更新迭代器 else ++it; } -
提前规划容量 :vector/string 批量插入前调用
reserve(n),避免扩容导致全迭代器失效; -
选择稳定迭代器容器:频繁插删且需迭代器稳定时,优先用list、unordered_map/set;
-
避免持有临时容器迭代器:局部容器销毁后,其迭代器变为"悬垂迭代器",不可使用。
四、适配器:封装现有组件,改变接口
适配器(Adapters)是对现有容器的"包装",改变其接口或功能,不直接存储数据,依赖底层容器实现:
| 适配器 | 底层容器默认选择 | 核心特性(接口) | 适用场景 |
|---|---|---|---|
| stack(栈) | deque | 后进先出(LIFO),支持push/pop/top | 函数调用栈、表达式求值、回溯算法 |
| queue(队列) | deque | 先进先出(FIFO),支持push/pop/front | 任务队列、消息队列、广度优先搜索(BFS) |
| priority_queue(优先队列) | vector | 优先级最高元素先出,支持push/pop/top | 堆排序、迪杰斯特拉算法、TopK问题 |
关键说明:
-
stack/queue 底层默认用deque,因其支持双端高效增删,且避免vector扩容的开销;
-
priority_queue 底层基于vector实现的"大根堆"(默认),可通过仿函数指定为小根堆:
cpp// 小根堆 priority_queue<int, vector<int>, greater<int>> minHeap;
五、容器选择策略:按需选型
选择容器的核心原则:匹配需求与容器性能特性,以下是常见场景的最优选择:
| 需求场景 | 推荐容器 |
|---|---|
| 动态数据、频繁随机访问、尾部增删 | vector |
| 固定大小、性能敏感、栈内存存储 | array |
| 中间频繁插入/删除、无需随机访问 | list |
| 双端增删频繁、需随机访问 | deque |
| 字典映射、键唯一、有序存储 | map |
| 字典映射、键唯一、追求高效访问(无需有序) | unordered_map |
| 去重、唯一性判断、有序存储 | set |
| 可重复元素、有序存储 | multiset/multimap |
| 栈/队列操作 | stack/queue(底层deque) |
| 优先级排序、TopK问题 | priority_queue(底层vector) |
六、实战技巧总结
- vector 性能优化 :批量插入前用
reserve(n)预分配容量,减少扩容次数; - 元素删除安全写法 :依赖
erase返回值更新迭代器,避免迭代器失效; - 复杂对象优先用 emplace_back:避免临时对象拷贝/移动开销,提升效率;
- 哈希容器优化 :通过
reserve(n)减少rehash,自定义哈希函数降低冲突; - 避免过度依赖 vector:中间频繁插删时,list效率远高于vector;
- map 与 unordered_map 选型:需有序/范围查询用map,需高效查找用unordered_map。
总结
STL的核心价值在于"泛型、复用、高效",掌握其关键在于理解容器底层实现 与性能特性。本文覆盖容器分类、核心原理、迭代器使用、适配器特性与实战技巧,建议结合代码练习(如容器增删查改、算法结合迭代器使用)加深理解。STL的学习无需死记硬背,重点在于"按需选型、避坑高效",熟练运用后能大幅提升编码效率与代码质量。