C++中分配器allocator的实现
前言标准容器vector、list、map都要申请和释放内存。这些内存从哪里来标准库并没有写死而是抽象成一个可替换的组件——分配器allocator。容器只依赖分配器提供申请 N 个元素的空间释放这块空间两个动作具体策略malloc、内存池、共享内存、竞技场 arena全部交给外部决定。这个设计的意义在于把内存策略与数据结构解耦。一个用自由链表free list做小对象缓存的分配器能让std::listint的性能提高数倍而容器代码一行都不用改。本文从分配器的抽象接口讲起讲清allocator_traits为什么存在、rebind解决什么问题再给出一份可直接编译运行的自定义分配器实现。一、为什么需要分配器抽象先看裸new/delete的三个问题每次都向操作系统或通用堆申请。std::listint插入 100 万个节点 100 万次operator new每次都要走堆管理器加锁、查找空闲块开销巨大。内存碎片。大量小对象反复申请释放会把堆切得七零八落最终总空闲够但拿不出连续块。每个对象都带元信息。通用堆为了free时需要知道大小通常在每个块前面放一个 header小对象的额外开销比例极高一个 16 字节的对象可能要额外 16 字节 header。分配器允许我们针对具体场景做优化。经典的 SGI STL 二级分配器思路是大于 128 字节走一级直接malloc小于等于 128 字节走二级按 8 字节对齐切成 16 个自由链表。这就是分配器存在的第一个理由。第二个理由是对象的构造与释放要分开。容器扩容时需要申请原始内存 → 在上面逐个构造元素两步。如果只有new构造和分配就绑死了vector扩容时就没法做到多申请一块把旧元素 move 过去再析构旧的只能每个元素都重新构造一遍。二、分配器的接口演进2.1 从重接口到最小接口早期std::allocator要求提供一整套成员allocate/deallocate/construct/destroy/max_size/rebind。C17 起construct、destroy、max_size被弃用C20 正式删除。原因是这些成员本就可以由std::allocator_traits提供默认实现强制要求反而增加了编写自定义分配器的负担。2.2 C11 之后的最小接口现代分配器只需要四个要素成员作用value_type元素类型allocator_traits据此推导allocate(n)分配n * sizeof(T)字节返回未构造的原始内存deallocate(p, n)释放从allocate得到的空间拷贝构造 /operator判断两个分配器实例能否互相释放内存如果自定义分配器只提供 allocate / deallocatestd::allocator_traits会自动补上construct、destroy、max_size、select_on_container_copy_construction、rebind等一大堆默认行为。这就是allocator_traits的核心价值——向后兼容 减轻实现负担。2.3 allocator_traits 与 rebind为什么容器不直接调用alloc.allocate(n)而要通过std::allocator_traitsAlloc::allocate(alloc, n)为了在不改变分配器定义的前提下给它补上缺失的成员为了统一调用点让标准库可以后续演进例如增加allocate_at_least。rebind解决的问题std::listT的节点类型是ListNodeT不是T。容器拿到的是allocatorT却需要allocatorListNodeT。allocator_traitsAlloc::rebind_allocU就是从Alloc得到元素类型换成U的分配器的标准途径。templateclass Tstruct MyAlloc {using value_type T;T* allocate(std::size_t n);void deallocate(T* p, std::size_t n);// 现代写法提供模板化拷贝构造rebind 由 traits 自动完成templateclass U MyAlloc(const MyAllocU) noexcept {}};// allocator_traitsMyAllocint::rebind_allocdouble MyAllocdouble注意那个templateclass U MyAlloc(const MyAllocU)转换构造函数——它是让rebind_alloc能工作的关键少了它容器会在编译期报无法从 MyAllocint 转换到 MyAllocNode。三、有状态分配器与传播策略分配器可以携带状态比如指向同一个内存池的指针。要让它工作所有rebind出来的版本都必须能访问到同一份状态templateclass Tclass PoolAlloc {public:using value_type T;explicit PoolAlloc(Pool* pool) noexcept : pool_(pool) {}templateclass UPoolAlloc(const PoolAllocU other) noexcept : pool_(other.pool()) {} // 传递状态T* allocate(std::size_t n);void deallocate(T* p, std::size_t n) noexcept;Pool* pool() const noexcept { return pool_; }private:Pool* pool_;};templateclass T, class Ubool operator(const PoolAllocT a, const PoolAllocU b) noexcept {return a.pool() b.pool(); // 能互相释放的前提同一个池}为什么必须重载因为std::vector在 move 赋值时如果两个容器的分配器不相等就不能把内存块直接偷过来必须逐元素搬移。分配器相等是可以偷内存的必要条件。几个传播 traits决定容器拷贝/移动/交换时分配器怎么处理traits含义propagate_on_container_copy_assignment拷贝赋值时用源分配器替换自己的propagate_on_container_move_assignmentmove 赋值时是否可替换true才能偷内存is_always_equal所有实例都相等 → 容器可以省掉存储分配器的空间空基类优化std::allocator的is_always_equal为true因此std::vectorint默认不额外存储分配器对象sizeof(std::vectorint)仍是 3 个指针。四、代码实战实战 1最小的可用分配器// minimal_alloc.cpp — g -stdc17 minimal_alloc.cpp -o minimal_alloc#include cstddef#include iostream#include memory#include new#include vectortemplateclass Tstruct LoggingAlloc {using value_type T;LoggingAlloc() noexcept default;templateclass U LoggingAlloc(const LoggingAllocU) noexcept {} // rebind 必需T* allocate(std::size_t n) {std::cout allocate n * sizeof(T) bytes\n;return static_castT*(::operator new(n * sizeof(T))); // 只分配不构造}void deallocate(T* p, std::size_t n) noexcept {std::cout deallocate n * sizeof(T) bytes\n;::operator delete(p); // 只释放不析构}};templateclass T, class Ubool operator(const LoggingAllocT, const LoggingAllocU) noexcept { return true; }int main() {std::vectorint, LoggingAllocint v;for (int i 0; i 10; i) v.push_back(i);std::cout size v.size() cap v.capacity() \n;return 0;}运行输出能直观看到vector的倍增扩容过程容量按 1、2、4、8、16 增长每次扩容都伴随一次allocate加一次deallocate。这也顺便解释了为什么vector要reserve。实战 2一个定长块自由链表分配器freelist allocator这是 SGI STL 二级分配器的核心思想写一个简化版本// freelist_alloc.cpp — g -stdc17 freelist_alloc.cpp -o freelist_alloc#include cstddef#include iostream#include list#include vector// 每个空闲块**复用自身的存储**保存 next 指针 —— 零额外开销// 这是自由链表分配器的精髓不为链表节点额外分配内存struct FreeNode {FreeNode* next;};templatestd::size_t BlockSize, std::size_t ChunkCount 64class FixedPool {public:FixedPool() default;FixedPool(const FixedPool) delete;FixedPool operator(const FixedPool) delete;~FixedPool() {// 只释放 chunk 链表块本身跟着 chunk 一起释放while (chunks_) {void* p chunks_;chunks_ *static_castvoid**(chunks_);::operator delete(p);}}void* allocate() {if (!free_) refill();FreeNode* node free_;free_ node-next; // 摘下头节点 —— O(1)return node;}void deallocate(void* p) noexcept {auto* node static_castFreeNode*(p);node-next free_; // 头插归还 —— O(1)free_ node;}// 槽位大小必须 指针大小否则存不下 next// 并且向上取整到 max_align_t 的倍数保证每一块都满足最大对齐static constexpr std::size_t slotSize() {constexpr std::size_t base BlockSize sizeof(FreeNode) ? sizeof(FreeNode) : BlockSize;constexpr std::size_t align alignof(std::max_align_t);return (base align - 1) / align * align;}private:void refill() {// 一次向系统申请一大块切成 ChunkCount 个块挂到自由链表上const std::size_t slot slotSize();char* raw static_castchar*(::operator new(slot * ChunkCount));// 用第一块的空间保存 chunk 链表指针*reinterpret_castvoid**(raw) chunks_;chunks_ raw;for (std::size_t i 1; i ChunkCount; i) {auto* node reinterpret_castFreeNode*(raw i * slot);node-next free_;free_ node;}}FreeNode* free_ nullptr;void* chunks_ nullptr;};// 把 pool 引用注入到分配器里templateclass T, class PoolTclass PoolAlloc {public:using value_type T;explicit PoolAlloc(PoolT* pool) noexcept : pool_(pool) {}templateclass UPoolAlloc(const PoolAllocU, PoolT other) noexcept : pool_(other.pool()) {}T* allocate(std::size_t n) {if (n ! 1) {// 池只服务于单元素分配list/map 节点批量走通用堆return static_castT*(::operator new(n * sizeof(T)));}return static_castT*(pool_-allocate());}void deallocate(T* p, std::size_t n) noexcept {if (n ! 1) { ::operator delete(p); return; }pool_-deallocate(p);}PoolT* pool() const noexcept { return pool_; }private:PoolT* pool_;};templateclass T, class U, class Pbool operator(const PoolAllocT, P a, const PoolAllocU, P b) noexcept {return a.pool() b.pool();}templateclass T, class U, class Pbool operator!(const PoolAllocT, P a, const PoolAllocU, P b) noexcept {return !(a b);}int main() {// list 节点的真实大小由实现决定这里取一个上界即可using NodePool FixedPoolsizeof(void*) * 4;NodePool pool;{using Alloc PoolAllocint, NodePool;std::listint, Alloc lst{Alloc(pool)};for (int i 0; i 5; i) lst.push_back(i);std::cout list size lst.size() \n;} // list 析构节点全部归还到 pool但 pool 不向系统释放return 0;}这个分配器的意义在于100 万次push_back只向系统申请1000000 / 64 ≈ 15625次大块把堆操作次数降低两个数量级而且因为块大小固定几乎不产生碎片。实战 3C17 的 pmr —— 标准库自带的多态分配器如果不方便给每个容器写分配器类型参数可以用std::pmrpolymorphic memory resourcealignas(std::max_align_t) unsigned char buffer[4096]; // 栈上缓冲区std::pmr::monotonic_buffer_resource pool{buffer, sizeof(buffer)};std::pmr::vectorint v{pool}; // 只传运行期指针for (int i 0; i 100; i) v.push_back(i); // 完全不碰全局堆// pool 析构时整块回收无需逐次 freepmr把分配器从类型的一部分变成运行期对象所有pmr容器共享同一个类型运行时通过memory_resource指针分派。代价是每个容器多存一个指针且分派走虚函数。常见坑点坑点 1allocate 之后立刻构造对象templateclass TT* allocate(std::size_t n) {T* p static_castT*(::operator new(n * sizeof(T)));for (std::size_t i 0; i n; i) new (p[i]) T(); // ❌ 分配器不该构造return p;}分配器只负责原始内存构造是容器或construct_at的事。在分配器里构造会让reserve被迫构造全部元素语义就错了deallocate时容器还会先析构再交给分配器造成双重析构。✅ 正确写法只分配不构造。T* allocate(std::size_t n) { return static_castT*(::operator new(n * sizeof(T))); }void deallocate(T* p, std::size_t) noexcept { ::operator delete(p); }坑点 2分配器生命周期短于容器std::vectorint, PoolAllocint, PoolT makeVec() {PoolT pool; // ❌ 局部对象std::vectorint, PoolAllocint, PoolT v{PoolAllocint, PoolT(pool)};return v; // 返回后 pool 已析构v 持有悬空指针}✅ 正确写法分配器持有的池必须活得比容器久静态存储期或由调用方持有。坑点 3allocate 的 n 与 deallocate 的 n 不一致标准要求deallocate(p, n)的n与allocate(n)相同。有状态池分配器如果按n走不同代码路径如实战 2 里n ! 1的分支一旦不匹配就会拿operator delete去释放池里的块或者反过来——立刻崩溃。最后提醒一句std::allocator直接使用::operator new现代 glibc / jemalloc / tcmalloc 对小对象已有很好的缓存。除非 profile 显示分配确实是瓶颈否则不要盲目替换。先测量再优化。总结概念要点最小接口value_typeallocatedeallocate 转换构造 operatorallocator_traits补齐默认实现让自定义分配器只需写两个函数rebind容器需要把allocatorT变成allocatorNodeT有状态分配器必须重载operator且生命周期要够长传播 traits决定容器拷贝/移动时能否偷内存pmr类型无关的多态分配器代价是多一个指针分配器的价值不在能编译而在把内存策略变成可插拔的一等公民。理解了容器只认 allocate/deallocate 与 construct/destroy你就理解了标准库几乎所有容器的内存行为。

相关新闻

OCP NIC 3.0规范解析:机械尺寸、散热与信号设计实战指南

OCP NIC 3.0规范解析:机械尺寸、散热与信号设计实战指南

简介:《OCP NIC 3.0 Design Specification Version 1.5.0 - Release》是一份由OCP服务器工作组下属网卡子组编写的官方设计规范,面向服务器硬件设计、网卡研发与数据中心基础设施工程师。文档发布于2024年9月,系统定义了OCP网卡3.0版的机械结…

2026/10/15 4:11:47 阅读更多 →
SEO工程化实践:从爬虫原理到技术优化与排名修复的完整指南

SEO工程化实践:从爬虫原理到技术优化与排名修复的完整指南

简介:《SEO艺术》第四版是由Eric Enge、Stephan Spencer和Jessie Stricchiola三位行业资深专家合著的经典SEO指南,适合从零起步的市场运营人员、网站站长,以及希望系统提升搜索排名的技术开发者。全书不仅梳理了搜索引擎排序的基本原理&#…

2026/10/15 4:11:47 阅读更多 →
STARTER实战:用命令行模板把项目初始化从两小时压到两分钟

STARTER实战:用命令行模板把项目初始化从两小时压到两分钟

简介:STARTER是西门子推出的变频器调试与维护软件,主要面向电气工程师、自动化设备调试人员,用于完成变频器参数设置、在线诊断、固件升级等操作。该资源以单个doc文档压缩打包,包体仅111KB,内容精炼,适合快…

2026/10/15 4:10:47 阅读更多 →

最新新闻

静态路由实现全网可达:从路由表原理到配置与排错

静态路由实现全网可达:从路由表原理到配置与排错

做网络项目最烦的是什么?设备连好线、接口全亮,结果PC之间互相ping不通,而且怎么查都看不出问题。干这行久了你就明白,八成是路由表没配周全——数据包走到中间某台路由器,转发表里没有目标网段,报文直接被…

2026/10/15 4:58:06 阅读更多 →
AI检测原理与合规降AI率:继续教育论文写作实战指南

AI检测原理与合规降AI率:继续教育论文写作实战指南

那篇论文连我自己看完都愣了:排版干净,结构完整,引用规范,A同学写的是继续教育本科的毕业设计说明,前后只用了一个下午。学校系统给出的AI疑似度是78%,导师发来消息:“内容是你自己写的吗&#…

2026/10/15 4:58:06 阅读更多 →
Apollo-11制导计算机源码解析:从绳存储器到任务调度的工程实践

Apollo-11制导计算机源码解析:从绳存储器到任务调度的工程实践

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

2026/10/15 4:58:06 阅读更多 →
Binder机制核心解析:从mmap一次拷贝到AIDL跨进程通信

Binder机制核心解析:从mmap一次拷贝到AIDL跨进程通信

Binder这套东西,搞Android的人早晚都得啃。很多人刚开始接触它的时候,看到的都是“进程间通信”“内核驱动”“mmap”这种抽象名词,一头扎进去就被C、C、Java三层代码绕晕了。但你要是真的花点时间把它的设计思路捋清楚,再回头去看…

2026/10/15 4:58:06 阅读更多 →
金融行业证书有哪些?按岗位方向分类梳理

金融行业证书有哪些?按岗位方向分类梳理

金融行业的证书数量多、名称混杂,初次接触时容易把从业资格、水平评价、培训证书混在一起。一个比较清晰的入手方式是先看制度属性:法定准入类、水平评价类、能力提升类。三类证书对应的岗位方向、报考条件和用途并不相同,先分清属性&#xf…

2026/10/15 4:58:06 阅读更多 →
PLC交通灯状态机设计:告别延时链,构建可靠时序逻辑

PLC交通灯状态机设计:告别延时链,构建可靠时序逻辑

简介:本资源是一份面向电气工程及其自动化专业本科生的PLC实践教学材料,聚焦S7-1200可编程控制器在八段LED显示与智能交通灯控制两大典型场景中的综合应用,解决初学者对TIA Portal V13软件操作、梯形图编程、HMI人机交互及仿真验证等核心技能…

2026/10/15 4:57:06 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练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/14 7:53:34 阅读更多 →