因std::allocator有锁、元数据开销和碎片,固定块内存池可实现o(1)分配释放、零锁、无外部碎片;适用于高频创建销毁同类型对象,但需权衡适用场景。

为什么不用 std::allocator 而要手写固定块内存池?
因为 std::allocator 每次 new 都走系统堆,有锁、有元数据开销、有碎片;而固定大小块池能实现 O(1) 分配/释放、零锁(单线程或带局部缓存时)、彻底避免外部碎片。典型场景是高频创建销毁同类型对象,比如游戏实体、网络包缓冲、日志条目。
但别一上来就写——先确认你真需要它:如果对象大小不固定、生命周期差异极大、或并发写入线程数 > 4,池的维护成本可能反超收益。
malloc 预分配 + 自由链表是最简可行路径
核心就是两件事:一次向系统申请大块内存,再用指针链把空闲块串起来。不需要虚函数、不依赖 RTTI,C++98 就能跑。
- 预分配用
operator new(size_t)(不是new T),避免构造调用 - 每个空闲块头部塞一个
void*指针,指向下一个空闲块(即“自由链表”) - 分配时取链表头,释放时插回链表头——无遍历、无查找
- 块大小必须 ≥
sizeof(void*),否则存不下指针;实际建议对齐到 8 或 16 字节
示例关键片段:
立即学习“C++免费学习笔记(深入)”;
char* pool = static_cast<char*>(operator new(block_size * block_count));
std::vector<void*> free_list;
for (size_t i = 0; i < block_count; ++i) {
free_list.push_back(pool + i * block_size);
}
// 释放第 i 块:free_list.push_back(pool + i * block_size);
// 分配:auto p = free_list.back(); free_list.pop_back();
多线程下 free_list 竞态怎么破?
直接用 std::atomic 改造自由链表头指针,比加互斥锁快得多,且避免线程阻塞。这是 C++11 后最推荐做法。
- 把自由链表变成单向无锁栈:用
std::atomic<void>::compare_exchange_weak</void>原子更新头指针 - 每次分配:读头指针 → 读下一节点 → CAS 更新头为下一节点
- 每次释放:读当前头 → 写本块 next 指向当前头 → CAS 更新头为本块
- 注意:块内偏移必须一致,比如都从 offset=0 开始存指针,否则不同线程看到的 next 地址错乱
别用 std::shared_ptr 或引用计数管理池本身——它自己就得是静态或全局生存期,否则析构顺序引发 UAF。
对象构造/析构不在池逻辑里,这点极易漏掉
内存池只管“内存”,不管“对象”。分配出来的是原始内存,必须显式调用 new(p) T(args...);释放前必须显式调用 p->~T()。漏掉任一环节,就会出现未定义行为或资源泄漏。
- 封装成类模板时,
allocate()返回void*,不负责构造;deallocate()接收void*,不负责析构 - 用户侧需配合使用 placement new 和显式析构,或者用 RAII 包装器(如
std::unique_ptr<t pooldeleter></t>) - 千万别在池类析构里循环调用对象析构——你根本不知道哪些块已被分配出去、哪些还活着
最常被忽略的是:池销毁时,若仍有活跃对象没调用析构,后续复用该内存会触发二次构造,UB 就藏在这儿。








