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_map

vector 的关键行为

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(理由见 容器字符串与内存):

STLUE
std::vectorTArray
std::unordered_mapTMap
std::stringFString(FName 做 key)
std::shared_ptrTSharedPtr
std::pmrTInlineAllocator / 自定义

规则:在 UE 代码里用 UE 容器,混用会导致内存统计对不上、分析工具看不到。

常见坑

坑说明
默认用 std::list缓存灾难,几乎总有更好的选择
不 reserve反复扩容搬移
扩容后继续用旧迭代器/指针全部失效
map 只为了查 keyunordered_map 更快(除非需要有序)
自定义类型没写好的哈希退化成链表,O(n)
频繁 new/delete 小对象应该池化
在头文件里定义容器且被广为包含改动触发大范围重编
用 std::vector<bool>它是位压缩特化,行为与别的 vector 不同