news 2026/10/7 9:39:01

C++基本组件之内存池详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++基本组件之内存池详解

前言

内存池(memory pool)要解决的问题很具体:通用分配器(general-purpose allocator,也就是malloc/free或operator new/operator delete)为了应付任意大小、任意生命周期的请求, 内部必须维护元数据、分箱(bin)、合并空闲块(coalescing),并且为了线程安全通常要加锁。 如果你的程序是"反复申请、释放大量同尺寸、短生命周期的小对象",这些都是纯开销。

一个常见误解是"内存池就是自己写个operator new"。不是。重载operator new只是换了入口, 真正决定性能的是池的分配策略:块大小如何取整、空闲块如何组织、何时向系统批量取内存。 另一个误解是"内存池一定更快"——单线程下它确实通常更快,但多线程共享同一个池时, 一把互斥量带来的竞争可能比malloc的线程缓存还糟。

本文分三层讲:先讲定长池(fixed-size pool)的核心结构"侵入式空闲链表", 再给出一个完整可编译的实现,最后介绍 C++17 标准库里已经提供的池设施std::pmr。

一、核心思想:侵入式空闲链表

定长池只需要三样东西:


  1. 一整块从系统申请来的连续内存,按固定块大小切成 N 份;

  2. 一条空闲块链表,把所有未分配的块串起来;

  3. 一个"下一个空闲块"的头指针。


关键技巧是侵入式(intrusive):next指针直接存放在空闲块自身的字节里, 而不是额外开一个数组。块被分配出去时,那块内存里的next就被用户的对象覆盖; 块被归还时,再在原地重建next。这样每个块的开销是零——没有每块的元数据, 也没有额外的链表节点。

这套结构决定了池的复杂度都是 O(1):分配是从链表头摘一个,释放是往链表头插一个, 没有任何查找或合并。代价是:


  • 块大小固定,只能服务一种尺寸(或一个尺寸档位)的请求;

  • 块大小的下限是sizeof(void*),否则装不下next指针;

  • 池本身不保证线程安全,多线程共享时必须自己加锁。


对比项operator new/delete定长内存池
单次分配复杂度实现相关,需要在空闲链/分箱里查找O(1) 摘头
每块元数据有(大小、边界标记等,实现定义)无
合并空闲块支持不需要(块等大)
内存碎片会产生外部碎片池内的块等大,无外部碎片
线程安全标准要求可多线程使用默认不安全,需自行加锁
归还给系统的时机视实现而定通常等池析构/release()

二、对齐与块尺寸

内存池最容易写错的地方是对齐。placement new(new (p) T(...))要求p满足alignof(T); 如果块大小取成 9 字节这种不是对齐数倍数的值,第 2 块开始的地址就不再是 8 的倍数, 在上面构造double或指针成员就是未定义行为(UB)。

正确的做法是把块大小向上取整到alignof(std::max_align_t)的倍数 (std::max_align_t在<cstddef>中)。alignof的结果一定是 2 的幂, 所以可以用位运算做取整:

constexpr std::size_t align_up(std::size_t n, std::size_t align) noexcept { return (n + align - 1) & ~(align - 1); }

C++17 的::operator new(size)返回的内存至少满足alignof(std::max_align_t), 所以按这个对齐取整,池内任意块都可以安全地承载"基本对齐"的类型。 如果类型是过度对齐(over-aligned)的(比如带alignas(64)), 就需要 C++17 引入的对齐版本:::operator new(size, std::align_val_t(align))和对应的::operator delete(ptr, std::align_val_t(align))(都在<new>里)。

三、完整实现

下面是一个可直接编译的定长池,以及叠在它上面的类型安全包装。

// 适用:C++17;GCC 13 / Clang 17 / MSVC 19.3x 均可编译 // 编译:g++ -std=c++17 -Wall -Wextra -pedantic pool.cpp -o pool #include <cstddef> #include <iostream> #include <new> #include <utility> namespace { constexpr std::size_t align_up(std::size_t n, std::size_t align) noexcept { return (n + align - 1) & ~(align - 1); } // 空闲块就地存放的结构:一个指针,指向下一个空闲块 struct FreeNode { FreeNode* next; }; } // namespace class FixedPool { public: FixedPool(std::size_t block_size, std::size_t block_count) : block_size_(align_up(block_size < sizeof(FreeNode) ? sizeof(FreeNode) : block_size, alignof(std::max_align_t))), block_count_(block_count), free_head_(nullptr), raw_(nullptr) { if (block_count_ == 0) { return; } // 一次性向系统要一整块内存,之后不再和分配器打交道 raw_ = static_cast<unsigned char*>(::operator new(block_size_ * block_count_)); for (std::size_t i = 0; i < block_count_; ++i) { void* block = raw_ + i * block_size_; ::new (block) FreeNode{free_head_}; // 在空闲块上建立 FreeNode 对象 free_head_ = static_cast<FreeNode*>(block); } } ~FixedPool() { ::operator delete(raw_); } // 传 nullptr 是合法的空操作 FixedPool(const FixedPool&) = delete; FixedPool& operator=(const FixedPool&) = delete; // 池耗尽时返回 nullptr,而不是抛异常 void* allocate() noexcept { if (free_head_ == nullptr) { return nullptr; } FreeNode* node = free_head_; free_head_ = node->next; return node; } void deallocate(void* p) noexcept { if (p == nullptr) { return; } ::new (p) FreeNode{free_head_}; // 重新建立 FreeNode,结束被归还对象的生命周期 free_head_ = static_cast<FreeNode*>(p); } std::size_t block_size() const noexcept { return block_size_; } std::size_t block_count() const noexcept { return block_count_; } std::size_t available() const noexcept { // O(n),仅供调试/统计 std::size_t n = 0; for (const FreeNode* p = free_head_; p != nullptr; p = p->next) { ++n; } return n; } private: std::size_t block_size_; // 已按 max_align_t 取整 std::size_t block_count_; FreeNode* free_head_; // 空闲链表头 unsigned char* raw_; // 整块原始内存 }; // 类型安全包装:负责构造与析构,FixedPool 只负责字节 template <class T> class ObjectPool { public: static_assert(alignof(T) <= alignof(std::max_align_t), "本示例只支持基本对齐的类型;过度对齐类型请改用 C++17 的 aligned operator new"); explicit ObjectPool(std::size_t count) : pool_(sizeof(T), count) {} template <class... Args> T* create(Args&&... args) { void* p = pool_.allocate(); if (p == nullptr) { return nullptr; } try { return new (p) T(std::forward<Args>(args)...); } catch (...) { pool_.deallocate(p); // 构造函数抛异常时把块还回去,否则这块内存永久泄露 throw; } } void destroy(T* obj) noexcept { if (obj == nullptr) { return; } obj->~T(); // 显式调用析构函数 pool_.deallocate(static_cast<void*>(obj)); } std::size_t available() const noexcept { return pool_.available(); } private: FixedPool pool_; }; struct Vec2 { double x; double y; Vec2(double x_, double y_) noexcept : x(x_), y(y_) {} }; int main() { ObjectPool<Vec2> pool(4); std::cout << "块大小 = " << sizeof(Vec2) << ",初始可用 = " << pool.available() << '\n'; Vec2* a = pool.create(1.0, 2.0); Vec2* b = pool.create(3.0, 4.0); if (a != nullptr && b != nullptr) { std::cout << a->x + b->y << '\n'; // 1.0 + 4.0 } pool.destroy(a); pool.destroy(b); std::cout << "释放后可用 = " << pool.available() << '\n'; return 0; }

逐行核对几个点:


  • ::new (block) FreeNode{free_head_}是布局 new(placement new),


它不分配内存,只在给定地址上构造对象;FreeNode是聚合类型,用花括号初始化列表。


  • block_size_ < sizeof(FreeNode) ? sizeof(FreeNode) : block_size保证块至少装得下一个指针。

  • 四个成员按block_size_、block_count_、free_head_、raw_的顺序声明,


构造函数的初始化列表顺序与之一致,避免-Wreorder警告(成员初始化顺序永远按声明顺序,不按列表顺序)。


  • ObjectPool::create的try/catch(...)里throw;是重新抛出当前异常,类型不变。


四、标准库里已有的池:std::pmr

C++17 在<memory_resource>里提供了现成的池设施,不必都自己写:


  • std::pmr::memory_resource:抽象基类,公开allocate/deallocate/is_equal,


子类实现do_allocate/do_deallocate/do_is_equal。


  • std::pmr::unsynchronized_pool_resource:单线程用,无锁,内部按尺寸档位管理多个小块池。

  • std::pmr::synchronized_pool_resource:多线程用,需要同步开销。

  • std::pmr::pool_options:可选项,有max_blocks_per_chunk和largest_required_pool_block


两个std::size_t成员,具体取值由实现决定。

// 需要 C++17 及以上 #include <cstddef> #include <memory_resource> #include <vector> int main() { std::pmr::unsynchronized_pool_resource pool; // 单线程,别跨线程用 std::pmr::polymorphic_allocator<int> alloc{&pool}; std::pmr::vector<int> v{alloc}; for (int i = 0; i < 1000; ++i) { v.push_back(i); // 扩容时的旧缓冲会归还给 pool,由 pool 复用 } pool.release(); // 一次性把所有内存还给上游(upstream) return 0; }

libstdc++ 里还有一个历史遗留的扩展__gnu_cxx::__pool_alloc(头文件<ext/pool_allocator.h>), 那是 SGI STL 时代的池式分配器;标准库的std::allocator现在只是operator new的薄包装, 本身不做池化——这一点常被搞混。

常见坑点


  1. 块大小没有对齐取整


❌ 直接按用户给的sizeof(T)切块,T 为 9 字节时第 2 块起地址不再对齐, 在上面 placement new 是 UB。

✅ 用align_up(size, alignof(std::max_align_t))把所有块对齐到max_align_t。


  1. 只归还内存,不调用析构函数


❌pool.deallocate(p);直接丢掉一个持有std::string成员的对象,它的堆内存永远不会释放。

✅p->~T(); pool.deallocate(p);—— 先显式析构,再还内存,顺序不能反。


  1. 同一块内存释放两次


❌ 归还两次会把同一个块两次插入空闲链表,链表形成环,下一次allocate可能拿到重复地址。

✅ 建立所有权约定:destroy之后立刻把指针置空,接口文档写清"不得重复释放"。 此外,给池类显式= delete拷贝构造与拷贝赋值,能从类型层面杜绝 "两个池对象管同一块内存"引发的双重释放。


  1. 块大小小于sizeof(void*)


❌ 用户申请 4 字节块并直接写FreeNode,越界覆盖相邻块的next字段。

✅ 用sizeof(FreeNode)做下限,并在文档里说明块实际会被放大。


  1. 构造抛异常时内存没归还


❌void* p = pool.allocate(); return new (p) T(args...);—— T 的构造抛了异常,p就永久丢失,池越用越少。

✅ 用try/catch(...)捕获,pool.deallocate(p)后重新throw。


  1. 把池当成线程安全的


❌ 多个线程直接共享一个FixedPool,free_head_的读改写没有任何同步—— 这是数据竞争,标准不保证任何行为,实际表现是链表断裂、块丢失或崩溃。

✅ 单线程用不加锁的池(每个线程一个thread_local池),必须共享时加std::mutex, 或直接用std::pmr::synchronized_pool_resource。


  1. 池对象先销毁,对象指针还在用


❌FixedPool析构后仍在访问之前allocate出来的块——那些内存已经还给系统, 访问是 UB。

✅ 池的生存期必须覆盖所有从它分配的对象;把它做成成员变量,声明在所有使用者之前。


  1. 过度对齐类型直接塞进池


❌ 对struct alignas(64) CacheLine { ... };用ObjectPool, 即使块大小取整到 64 的倍数,::operator new(size)也不保证 64 字节对齐。

✅ 改用 C++17 的::operator new(size, std::align_val_t(64)), 并在对应的::operator delete(ptr, std::align_val_t(64))里成对使用。

总结

关键点结论
空闲链表用侵入式链表,next直接存在空闲块里,零额外开销
块大小下限sizeof(void*),并向上取整到alignof(std::max_align_t)的倍数
分配/释放都是 O(1),无查找、无合并
对象生命周期placement new 构造,显式p->~T()析构,异常时要归还内存
线程安全池本身不安全,共享必须加锁或用std::pmr::synchronized_pool_resource
现成设施C++17 的std::pmr已提供池,除非有特殊需求不必手写


内存池的本质是用"块尺寸固定"这个约束换掉通用分配器的查找、合并和锁开销。 只要你的场景真的是同尺寸小对象高频分配,这笔交易就划算;一旦对象尺寸五花八门, 就该退回通用分配器或改用std::pmr的分档池。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 9:38:27

Windows 编程基础:动态链接库

专栏导航 上一篇&#xff1a;第1章&#xff0c;[Win32 章节]&#xff1a;Windows 简史 回到目录 下一篇&#xff1a;第1章 &#xff1a;第一个 Win32 程序&#xff0c;头文件 本节前言 对于本节所讲解的知识&#xff0c;有可能&#xff0c;你会需要时不时地参考本专栏的其…

作者头像 李华
网站建设 2026/10/7 9:38:09

多Agent系统路由与协同:Agent-Reach中间件设计与实践

多Agent系统跑起来之后&#xff0c;真正的麻烦才刚刚开始。Agent之间要互相找服务、要动态获取工具列表、要把任务精确投递给当前还“活着”的那一个……这些单机demo里完全不会暴露的问题&#xff0c;会随着Agent数量增长迅速变成团队的日常噩梦。Agent-Reach就是为解决这类问…

作者头像 李华
网站建设 2026/10/7 9:37:41

GoPro HERO12免官方App开发实战:BLE配对到Wi-Fi流媒体控制全攻略

最近做了一件有意思的事&#xff1a;不依赖GoPro官方App&#xff0c;直接从零把一台HERO12接进自己的控制链路——先通过蓝牙完成配对、拿到Wi-Fi凭据&#xff0c;再切到Wi-Fi通道走HTTP API控制拍照、录像、切换模式&#xff0c;最后把实时画面通过RTSP/HLS拉到播放器里。整套…

作者头像 李华