C++如何实现内存池分配器?(固定大小块管理)

4次阅读

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

C++如何实现内存池分配器?(固定大小块管理)

为什么不用 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 就藏在这儿。

text=ZqhQzanResources