当前位置: 首页 > news >正文

嵌入式现代C++教程——自定义分配器(Allocator)

嵌入式现代C++教程——自定义分配器(Allocator)

在嵌入式世界里,内存不是“无限”的抽屉,而是那只随时会嫌你占空间的行李箱。默认的new/malloc对我们友好吗?有时候很友好(没错,方便);但更多时候,它们是潜在的性能炸弹、不可预测的延迟来源、以及碎片化萌生地。于是,写一个“自定义分配器”——你自己的内存管理策略,就变成了工程师的基本修行。


为什么要自定义分配器?

想象一下这些场景:实时任务不能被偶发的malloc阻塞;启动阶段需要一次性分配若干对象以避免运行时分配;小对象分配频繁但大小恒定;或者你想把一大块内存划分给特定模块,便于追踪与回收。默认分配器往往无法同时满足:确定性、低内存占用、低碎片和高性能。

自定义分配器修改了像内存申请的具体模式,我们可以接入自己的固定大小池、栈式分配、快速分配器。在之前的博客中,我们的这些实现可以有效的避免堆碎片、提高局部性。


分配器的基础概念

分配器,归根结底就是两件事:分配(给出一段未被使用的内存)和释放(把内存归回池子)。在 C++ 里还要注意对齐(alignment)和对象的构造/析构(placementnew、显式destroy)。

常见策略有:Bump(指针上移)分配器Free-list(空闲链表/内存池)Stack(栈)分配器、以及更复杂的TLSF/分级位图等。下面我们通过代码直观对比。


最简单:Bump(线性)分配器 — 启动与临时用例的好朋友

特点:实现极其简单,分配 O(1),不支持释放单个对象(可以一次性重置)。适合启动期分配或者短周期任务。

// bump_allocator.h - 非线程安全,简单演示#include<cstddef>#include<new>#include<cassert>classBumpAllocator{char*start_;char*ptr_;char*end_;public:BumpAllocator(void*buffer,std::size_t size):start_(static_cast<char*>(buffer)),ptr_(start_),end_(start_+size){}void*allocate(std::size_t n,std::size_t align=alignof(std::max_align_t))noexcept{std::size_t space=end_-ptr_;std::uintptr_t p=reinterpret_cast<std::uintptr_t>(ptr_);std::size_t mis=p%align;std::size_t offset=mis?(align-mis):0;if(n+offset>space)returnnullptr;ptr_+=offset;void*res=ptr_;ptr_+=n;returnres;}voidreset()noexcept{ptr_=start_;}};

使用场景:启动时分配所有必要对象,后面不再释放;或临时缓冲池。记住:不能释放单个对象,除非你支持回滚到某个快照点(可以实现“标记/回滚”)。


固定大小内存池(Free-list)

当你有大量相同大小的小对象(例如消息节点、连接对象)时,固定大小内存池非常高效。每个槽(slot)大小固定,释放时把槽 push 回空闲链表。分配/释放都 O(1)。

// simple_pool.h - 单线程示例#include<cstddef>#include<cassert>#include<cstdint>classSimpleFixedPool{structNode{Node*next;};void*buffer_;Node*free_head_;std::size_t slot_size_;std::size_t slot_count_;public:SimpleFixedPool(void*buf,std::size_t slot_size,std::size_t count):buffer_(buf),free_head_(nullptr),slot_size_((slot_size<sizeof(Node*))?sizeof(Node*):slot_size),slot_count_(count){// 初始化空闲链表char*p=static_cast<char*>(buffer_);for(std::size_t i=0;i<slot_count_;++i){Node*n=reinterpret_cast<Node*>(p+i*slot_size_);n->next=free_head_;free_head_=n;}}void*allocate()noexcept{if(!free_head_)returnnullptr;Node*n=free_head_;free_head_=n->next;returnn;}voiddeallocate(void*p)noexcept{Node*n=static_cast<Node*>(p);n->next=free_head_;free_head_=n;}};

要点提示:slot_size应包含对齐与控制信息;线程安全时需要加锁或使用 lock-free 结构(复杂度上升)。内存利用率高,碎片少。


Stack(栈)分配器 — LIFO 场景的神器

当你分配/释放呈 LIFO(后进先出)模式时,栈分配器速度最快,可以释放一系列分配到某个“标记”为止。

// stack_allocator.h - 支持标记回滚classStackAllocator{char*start_;char*top_;char*end_;public:StackAllocator(void*buf,std::size_t size):start_(static_cast<char*>(buf)),top_(start_),end_(start_+size){}void*allocate(std::size_t n,std::size_t align=alignof(std::max_align_t))noexcept{// 类似Bump的对齐处理// ...}// 标记与回滚APIusingMarker=char*;Markermark()noexcept{returntop_;}voidrollback(Marker m)noexcept{top_=m;}};

适用:短生命周期链、任务栈式分配、帧分配(每帧分配,帧结束统一回收)。


用 C++ 风格包装(placement new 与析构)

分配器只提供原始内存;对象的构造/析构工作还是你的任务。示例如下:

#include<new>// placement new// allocate memory for T and constructtemplate<typenameT,typenameAlloc,typename...Args>T*construct_with(Alloc&a,Args&&...args){void*mem=a.allocate(sizeof(T),alignof(T));if(!mem)returnnullptr;returnnew(mem)T(std::forward<Args>(args)...);}// 销毁并归还内存(手动调用析构)template<typenameT,typenameAlloc>voiddestroy_with(Alloc&a,T*obj)noexcept{if(!obj)return;obj->~T();a.deallocate(static_cast<void*>(obj));}

重要:在嵌入式中,禁用异常或在异常敏感代码中使用noexcept的 allocate 是常见实践;因此好多实现返回nullptr而不是抛异常。


如何把自定义分配器和 STL 一起用

标准库的std::allocator接口在老标准中较为笨重。C++17/20 引入了std::pmr::memory_resource(更现代)用于替换默认分配策略。但在嵌入式里往往不启用完整的<memory_resource>,于是你可以自己:

  • 为容器写一个简单的 wrapper,内部使用你的池分配节点。
  • 或实现兼容std::allocator接口的类(需要一堆 typedef 和rebind),然后传给std::vector<T, MyAlloc<T>>

如果构建环境允许,优先考虑std::pmr—— 它语义更清晰,但开销与支持度要看你的平台。

http://www.jsqmd.com/news/344464/

相关文章:

  • 2026年中信广场附近白切鸡餐厅推荐,品质靠谱之选 - 工业品牌热点
  • [STM32L5] 【STM32L562E_DK开发板】--1.开箱与开发环境准备
  • 2026年全国口碑好的GEO优化公司推荐,专业GEO优化服务企业全解析 - myqiye
  • 如何选择可编程控制器?可编程控制器分类、性能指标介绍
  • JAX JIT编译:超越即时编译的静态计算图革命
  • 【EI稳定检索 | IEEE出版】第三届生成式人工智能与信息安全国际学术会议(GAIIS 2026)
  • 内存模型和名称空间(单独编译)
  • Obsidian Claudian Skills:打造真正会“思考”的个人知识库
  • 任天堂Switch二手游戏机上门回收,专业鉴定,公平交易,让您的闲置焕发新生! - 金诚数码回收
  • 京东e卡如何能够快速回收?三招让闲置卡券秒变现金 - 京顺回收
  • C++多文件编译:告别“一锅炖”,让代码管理更优雅
  • angular frequency角频率和frequency频率的区别
  • 【目标跟踪】基于matlab扩展多目标跟踪概率假设密度滤波(线性高斯混合实现)【含Matlab源码 15069期】
  • 2026年国内靠谱国企求职机构排名,口碑好性价比高的应届生求职机构盘点 - mypinpai
  • 相机标定原理与概念(先搞懂再动手)
  • 【Matlab】MATLAB if-else语句详解:二选一条件执行与简单分支逻辑应用
  • 轻杀青在普洱生茶中的时间逻辑基础 - 资讯焦点
  • Halcon几何测量集成详解
  • 深耕常州无锡镇江泰州制造企业,助力工厂抖音短视频拍摄运营+外贸TikTok拓客爆单 - 资讯焦点
  • 从GAP到剪枝:CNN全连接层分类技术演进与实战指南
  • 让 Claude 直接读写你的语雀知识库!这款开源工具太香了
  • 每日面试题分享178:如何解决页面接口大规模并发问题?
  • 深入解析CNN中的BN层:从稳定训练到前沿演进
  • postgreq sum(a) 如果a有null值会有什么影响
  • SpringBoot 优雅处理金额格式化:拦截器+自定义注解方案
  • Vue 2.3
  • *Turnitin
  • 基于MATLAB的油润滑轴承压力分布求解
  • python学习一:变量python简单数据类型
  • 稀疏化压缩测试:神经架构搜索在模型精简的精度损失验证工具热度解析