C++基本组件之内存池详解
前言内存池memory pool要解决的问题很具体通用分配器general-purpose allocator也就是malloc/free或operator new/operator delete为了应付任意大小、任意生命周期的请求 内部必须维护元数据、分箱bin、合并空闲块coalescing并且为了线程安全通常要加锁。 如果你的程序是反复申请、释放大量同尺寸、短生命周期的小对象这些都是纯开销。一个常见误解是内存池就是自己写个operator new。不是。重载operator new只是换了入口 真正决定性能的是池的分配策略块大小如何取整、空闲块如何组织、何时向系统批量取内存。 另一个误解是内存池一定更快——单线程下它确实通常更快但多线程共享同一个池时 一把互斥量带来的竞争可能比malloc的线程缓存还糟。本文分三层讲先讲定长池fixed-size pool的核心结构侵入式空闲链表 再给出一个完整可编译的实现最后介绍 C17 标准库里已经提供的池设施std::pmr。一、核心思想侵入式空闲链表定长池只需要三样东西一整块从系统申请来的连续内存按固定块大小切成 N 份一条空闲块链表把所有未分配的块串起来一个下一个空闲块的头指针。关键技巧是侵入式intrusivenext指针直接存放在空闲块自身的字节里 而不是额外开一个数组。块被分配出去时那块内存里的next就被用户的对象覆盖 块被归还时再在原地重建next。这样每个块的开销是零——没有每块的元数据 也没有额外的链表节点。这套结构决定了池的复杂度都是 O(1)分配是从链表头摘一个释放是往链表头插一个 没有任何查找或合并。代价是块大小固定只能服务一种尺寸或一个尺寸档位的请求块大小的下限是sizeof(void*)否则装不下next指针池本身不保证线程安全多线程共享时必须自己加锁。对比项operator new/delete定长内存池单次分配复杂度实现相关需要在空闲链/分箱里查找O(1) 摘头每块元数据有大小、边界标记等实现定义无合并空闲块支持不需要块等大内存碎片会产生外部碎片池内的块等大无外部碎片线程安全标准要求可多线程使用默认不安全需自行加锁归还给系统的时机视实现而定通常等池析构/release()二、对齐与块尺寸内存池最容易写错的地方是对齐。placement newnew (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); }C17 的::operator new(size)返回的内存至少满足alignof(std::max_align_t) 所以按这个对齐取整池内任意块都可以安全地承载基本对齐的类型。 如果类型是过度对齐over-aligned的比如带alignas(64) 就需要 C17 引入的对齐版本::operator new(size, std::align_val_t(align))和对应的::operator delete(ptr, std::align_val_t(align))都在new里。三、完整实现下面是一个可直接编译的定长池以及叠在它上面的类型安全包装。// 适用C17GCC 13 / Clang 17 / MSVC 19.3x 均可编译 // 编译g -stdc17 -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_castunsigned 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_castFreeNode*(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_castFreeNode*(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), 本示例只支持基本对齐的类型过度对齐类型请改用 C17 的 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::forwardArgs(args)...); } catch (...) { pool_.deallocate(p); // 构造函数抛异常时把块还回去否则这块内存永久泄露 throw; } } void destroy(T* obj) noexcept { if (obj nullptr) { return; } obj-~T(); // 显式调用析构函数 pool_.deallocate(static_castvoid*(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() { ObjectPoolVec2 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_}是布局 newplacement new它不分配内存只在给定地址上构造对象FreeNode是聚合类型用花括号初始化列表。block_size_ sizeof(FreeNode) ? sizeof(FreeNode) : block_size保证块至少装得下一个指针。四个成员按block_size_、block_count_、free_head_、raw_的顺序声明构造函数的初始化列表顺序与之一致避免-Wreorder警告成员初始化顺序永远按声明顺序不按列表顺序。ObjectPool::create的try/catch(...)里throw;是重新抛出当前异常类型不变。四、标准库里已有的池std::pmrC17 在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成员具体取值由实现决定。// 需要 C17 及以上 #include cstddef #include memory_resource #include vector int main() { std::pmr::unsynchronized_pool_resource pool; // 单线程别跨线程用 std::pmr::polymorphic_allocatorint alloc{pool}; std::pmr::vectorint 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的薄包装 本身不做池化——这一点常被搞混。常见坑点块大小没有对齐取整❌ 直接按用户给的sizeof(T)切块T 为 9 字节时第 2 块起地址不再对齐 在上面 placement new 是 UB。✅ 用align_up(size, alignof(std::max_align_t))把所有块对齐到max_align_t。只归还内存不调用析构函数❌pool.deallocate(p);直接丢掉一个持有std::string成员的对象它的堆内存永远不会释放。✅p-~T(); pool.deallocate(p);—— 先显式析构再还内存顺序不能反。同一块内存释放两次❌ 归还两次会把同一个块两次插入空闲链表链表形成环下一次allocate可能拿到重复地址。✅ 建立所有权约定destroy之后立刻把指针置空接口文档写清不得重复释放。 此外给池类显式 delete拷贝构造与拷贝赋值能从类型层面杜绝 两个池对象管同一块内存引发的双重释放。块大小小于sizeof(void*)❌ 用户申请 4 字节块并直接写FreeNode越界覆盖相邻块的next字段。✅ 用sizeof(FreeNode)做下限并在文档里说明块实际会被放大。构造抛异常时内存没归还❌void* p pool.allocate(); return new (p) T(args...);—— T 的构造抛了异常p就永久丢失池越用越少。✅ 用try/catch(...)捕获pool.deallocate(p)后重新throw。把池当成线程安全的❌ 多个线程直接共享一个FixedPoolfree_head_的读改写没有任何同步—— 这是数据竞争标准不保证任何行为实际表现是链表断裂、块丢失或崩溃。✅ 单线程用不加锁的池每个线程一个thread_local池必须共享时加std::mutex 或直接用std::pmr::synchronized_pool_resource。池对象先销毁对象指针还在用❌FixedPool析构后仍在访问之前allocate出来的块——那些内存已经还给系统 访问是 UB。✅ 池的生存期必须覆盖所有从它分配的对象把它做成成员变量声明在所有使用者之前。过度对齐类型直接塞进池❌ 对struct alignas(64) CacheLine { ... };用ObjectPool 即使块大小取整到 64 的倍数::operator new(size)也不保证 64 字节对齐。✅ 改用 C17 的::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现成设施C17 的std::pmr已提供池除非有特殊需求不必手写内存池的本质是用块尺寸固定这个约束换掉通用分配器的查找、合并和锁开销。 只要你的场景真的是同尺寸小对象高频分配这笔交易就划算一旦对象尺寸五花八门 就该退回通用分配器或改用std::pmr的分档池。

相关新闻

alien-signals 响应式图的高性能调试:用可视化工具洞察依赖图拓扑

alien-signals 响应式图的高性能调试:用可视化工具洞察依赖图拓扑

alien-signals 响应式图的高性能调试:用可视化工具洞察依赖图拓扑在传统前端框架以“组件为中心”的时代,调试响应式问题相对直观:打开 Vue DevTools,点击左侧的组件树,查看当前组件的 data、props,在模板渲…

2026/10/7 9:39:28 阅读更多 →
天梯赛L3-2完美树:树形DP奇偶性与01价值贪心优化

天梯赛L3-2完美树:树形DP奇偶性与01价值贪心优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 9:39:28 阅读更多 →
一条命令关掉苹果智能,还你12GB硬盘空间:Mac用户正在疯传的RemoveMacAI到底靠不靠谱?

一条命令关掉苹果智能,还你12GB硬盘空间:Mac用户正在疯传的RemoveMacAI到底靠不靠谱?

如果你的Mac最近越用越卡,打开存储空间一看,系统数据那一栏悄无声息地吃掉了十几GB,别急着怪自己存了太多照片——十有八九是苹果智能(Apple Intelligence)干的。升级到macOS 27之后,很多用户发现一个细思恐…

2026/10/7 9:39:28 阅读更多 →

最新新闻

《论持久战》的精髓与要点毛泽东,1938年5月 · 针对抗日战争战略问题的系统性答复核心命题:不是“只要坚持就能胜利”,而是“在敌强我弱的客观起点上,靠矛盾转化与能动作战,把战略防御走成战略反攻

《论持久战》的精髓与要点毛泽东,1938年5月 · 针对抗日战争战略问题的系统性答复核心命题:不是“只要坚持就能胜利”,而是“在敌强我弱的客观起点上,靠矛盾转化与能动作战,把战略防御走成战略反攻

《论持久战》的精髓与要点毛泽东,1938年5月 针对抗日战争战略问题的系统性答复核心命题:不是“只要坚持就能胜利”,而是“在敌强我弱的客观起点上,靠矛盾转化与能动作战,把战略防御走成战略反攻”《论持久战》写于193…

2026/10/7 10:05:47 阅读更多 →
一文吃透 Linux 软件安装与 Vim 编辑器(含 Ubuntu/CentOS 实操)

一文吃透 Linux 软件安装与 Vim 编辑器(含 Ubuntu/CentOS 实操)

目录 一、在Linux中安装软件包的三种做法: 1.1 包管理器介绍 1.2 Linux的软件生态问题 二、为什么需要国内镜像源 国内主流开源镜像站汇总 三、yum与apt 3.1 Ubuntu: 3.2 安装源: 3.3 清理缓存 四、编辑器vim 4.1 vim的基本概念 4…

2026/10/7 10:05:47 阅读更多 →
五子棋设计

五子棋设计

一.创建窗口1.创建一个类和对象public class Gameui {public static void main(String[] args) {Gameui uinew Gameui();ui.showUI();}}2.创建showui方法public void showUI() {}3.创建一个窗口对象jf,对窗口的属性进行设置Myframe jfnew Myframe();//创…

2026/10/7 10:05:47 阅读更多 →
ponytail技术解析:从基础概念到工程应用

ponytail技术解析:从基础概念到工程应用

我无法根据当前输入生成符合要求的博文。原因如下:项目标题仅为“ponytail”,这是一个英文单词,直译为“马尾辫”,属于常见发型术语;项目正文为空;关键词为空;摘要描述为空;所谓“相…

2026/10/7 10:05:46 阅读更多 →
代码驱动白板视频:rough.js+Playwright+FFmpeg全链路实现

代码驱动白板视频:rough.js+Playwright+FFmpeg全链路实现

1. 这不是动画软件,是用代码一笔一划“写”出来的白板视频你见过的白板视频,大概率是用After Effects加手绘插件、或者用Explain Everything这类工具录屏生成的。但这次我要说的,是另一种路径:整条视频里每一根线条、每一个文字、…

2026/10/7 10:05:46 阅读更多 →
C# WinForm 数据库备份与恢复实战:SQL语句与文件操作两种方式详解

C# WinForm 数据库备份与恢复实战:SQL语句与文件操作两种方式详解

简介:这份资源是面向C# WinForm开发者的数据库备份与恢复实战Demo,基于VS2008实现,适合需要为桌面应用增加数据安全保障能力的初中级开发者参考。示例围绕两种主流方案展开:一是借助SQLDMO这一COM对象模型,通过SQLServ…

2026/10/7 10:04:45 阅读更多 →

日新闻

ROS2机械臂仿真与运动控制:从URDF建模到Gazebo实战全解析

ROS2机械臂仿真与运动控制:从URDF建模到Gazebo实战全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:01:58 阅读更多 →
用浏览器直接改ESP32的WiFi密码:NVS键值配置工具设计与实现

用浏览器直接改ESP32的WiFi密码:NVS键值配置工具设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:02:00 阅读更多 →
芯片封装缺陷检测:扫描声学显微镜(SAT)原理与实操指南

芯片封装缺陷检测:扫描声学显微镜(SAT)原理与实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:02:00 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 7:15:40 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 5:29:09 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 9:29:10 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 8:21:32 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 4:21:51 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 1:18:13 阅读更多 →