STL 容器与分配器
自动关联目录:STL 容器与分配器
选容器的本质是选内存布局:连续、链式、哈希、树。布局决定了缓存命中率,而缓存命中率在现代 CPU 上往往比算法复杂度更决定性。
一句话定位:默认 std::vector;只有明确的理由才换。绝大多数"要不要用 map"的纠结,答案都是"vector + sort"。
选型
| 容器 | 布局 | 查找 | 插入 | 适用 |
|---|---|---|---|---|
std::vector | 连续 | O(n) | 尾 O(1) | 默认 |
std::deque | 分段连续 | O(n) | 头尾 O(1) | 双端队列(见 实现双端队列) |
std::list | 双向链表 | O(n) | O(1) | 几乎不该用 |
std::map / set | 红黑树 | O(log n) | O(log n) | 需要有序遍历 |
std::unordered_map | 哈希表 | O(1) | O(1) | 只需要按 key 查 |
std::array | 栈上固定 | O(n) | — | 编译期已知大小 |
"链表为什么慢":每个节点一次随机内存访问,缓存全废。1000 个元素的 std::list 遍历,通常比 1000 个元素的 vector 慢一个数量级,即使两者都是 O(n)。
判断流程:
需要按 key 查? ──否──► vector
│是
需要有序遍历? ──是──► map
│否
unordered_mapvector 的关键行为
std::vector<Foo> v;
v.reserve(1000); // 预留,避免扩容搬移
v.emplace_back(a, b); // 原地构造,比 push_back(Foo(a,b)) 少一次移动
v.shrink_to_fit(); // 释放多余容量(可能搬移)
v.clear(); // 析构元素,保留容量| 操作 | 复杂度 | 注意 |
|---|---|---|
| 尾插 | 均摊 O(1) | 扩容时所有元素搬移 + 重分配 |
| 中间插入/删除 | O(n) | 搬移后续元素 |
erase 遍历删 | — | 用 it = v.erase(it),不要 ++it |
operator[] | O(1) | 不检查边界(at() 才检查) |
扩容搬移是隐藏的性能杀手:vector 里存大对象时,一次扩容要移动所有元素。存 std::unique_ptr 或索引能显著缓解。
迭代器失效速查
| 操作 | 失效范围 |
|---|---|
push_back(未扩容) | 无(除 end()) |
push_back(扩容) | 全部 |
insert | 插入点及之后 |
erase | 被删元素及之后 |
reserve / resize | 全部 |
clear | 全部 |
"扩容后全部失效"是最常见的悬垂迭代器来源——如果必须先 reserve,或者干脆用索引而不是迭代器。
unordered_map 的代价
| 陷阱 | 说明 |
|---|---|
| rehash | 元素数超 max_load_factor 就重建整个表,一次性很贵 |
| 哈希函数 | 默认 std::hash 对自定义类型要自己写 |
| 内存 | 每个桶有额外开销,小表浪费明显 |
| 迭代顺序 | 无序且不稳定 |
| 缓存 | 节点分散,缓存不友好 |
m.reserve(10000); // 避免 rehash
m.max_load_factor(0.7f); // 降低装载因子换速度小数据集(< 几十个元素)用 vector 线性查找往往更快——没有哈希开销,且缓存连续。只有元素多了才显出 O(1) 的优势。
分配器
分配器的作用是"把内存从哪来"这件事抽出来。默认 std::allocator 走 operator new → malloc。
template<typename T>
struct PoolAllocator
{
using value_type = T;
T* allocate(std::size_t n);
void deallocate(T* p, std::size_t n);
};
using FastVector = std::vector<Foo, PoolAllocator<Foo>>;| 分配器类型 | 适用 |
|---|---|
| 默认 | 通用 |
| 内存池 / 对象池 | 大量同尺寸对象、频繁创建销毁 |
| 单调 / 竞技场(arena) | 一整批对象同时释放(关卡、请求) |
| 栈分配器 | 临时缓冲,函数结束即回收 |
std::pmr::*(C++17) | 多态分配器,可运行时切换 |
std::pmr 是 C++17 之后的正解——过去自定义分配器会改变容器类型(vector<T, MyAlloc>),pmr 用类型擦除解决了这个问题:
std::pmr::monotonic_buffer_resource Pool(Buffer, sizeof(Buffer));
std::pmr::vector<int> v(&Pool);为什么 malloc 慢
| 原因 | 说明 |
|---|---|
| 全局锁 | 多线程分配要抢锁(现代分配器有线程缓存缓解) |
| 碎片 | 长期运行后空闲块不连续 |
| 系统调用 | 大块分配走 mmap / VirtualAlloc |
| 元信息 | 每个块有 header,且通常在数据前后(破坏缓存行) |
优化顺序:先减少分配次数,再换分配器。池化的收益主要来自"不再频繁 malloc",而不是"malloc 变快了"。
| 手段 | 做法 |
|---|---|
reserve | 一次性分配 |
| 对象池 | 复用对象,不真的释放 |
| arena | 一批一起分配一起释放 |
| 小对象放栈上 | 避免堆 |
| SSO(小字符串优化) | std::string 内建,短字符串不分配 |
与 UE 的对照
UE 用自己的容器而非 STL(理由见 容器字符串与内存):
| STL | UE |
|---|---|
std::vector | TArray |
std::unordered_map | TMap |
std::string | FString(FName 做 key) |
std::shared_ptr | TSharedPtr |
std::pmr | TInlineAllocator / 自定义 |
规则:在 UE 代码里用 UE 容器,混用会导致内存统计对不上、分析工具看不到。
常见坑
| 坑 | 说明 |
|---|---|
默认用 std::list | 缓存灾难,几乎总有更好的选择 |
不 reserve | 反复扩容搬移 |
| 扩容后继续用旧迭代器/指针 | 全部失效 |
map 只为了查 key | unordered_map 更快(除非需要有序) |
| 自定义类型没写好的哈希 | 退化成链表,O(n) |
| 频繁 new/delete 小对象 | 应该池化 |
| 在头文件里定义容器且被广为包含 | 改动触发大范围重编 |
用 std::vector<bool> | 它是位压缩特化,行为与别的 vector 不同 |
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/193baa90.html
更新于:2026年10月10日