结论:用自定义std::list(按地址升序维护)+ 固定大小内存池即可模拟空闲链表并统计碎片;外部碎片通过遍历相邻空闲块计算地址间隙,内部碎片统计拆分后不可用的残余块(size < MIN_ALLOC_SIZE)。
怎么用 C++ 模拟空闲链表并统计内存碎片
直接说结论:不用重写 malloc,用自定义
或结构体链表 + 固定大小内存池即可完成碎片统计。核心是把“空闲块”建模为带
和
的节点,按地址排序后遍历,计算相邻块之间的间隙(即外部碎片)和单个块内无法分配的剩余空间(即内部碎片)。
常见错误是把“碎片”等同于“小块数量多”——实际要看能否拼成目标请求尺寸。比如有 10 个 64B 空闲块,但你要分配 512B,它们之间地址不连续,就仍是碎片。
初始化一个大数组模拟堆,如
(1MB)
维护一个
,其中
首次将整个堆作为单个空闲块插入链表,
,
每次分配时从链表中查找合适块(首次适配/最佳适配),拆分后更新链表
释放时合并相邻空闲块(检查
和
是否紧邻)
如何识别和量化外部碎片(gap-based fragmentation)
外部碎片指空闲块之间存在的、因地址不连续而无法被利用的间隙。它不存储在空闲链表里,必须通过遍历排序后的空闲块推算。
关键点:空闲链表必须按
升序排列,否则 gap 无法计算。别用
存然后每次 sort —— 插入/删除开销大,改用
+ 手动插入保持有序,或
。
立即学习
“
C++免费学习笔记(深入)
”;
C知道
CSDN推出的一款AI技术问答工具
下载
遍历排序后的空闲块列表,对每对相邻节点
和
,计算
若
,说明存在外部碎片,累计到总 gap 字节数
可额外统计
的微小间隙数量(这类几乎无法用于任何分配)
注意:最后一个块之后、第一个块之前不计入 gap(不属于堆内可用范围)
内部碎片统计不能只看 malloc 对齐
内部碎片是已分配块中未被使用的部分,常源于对齐要求或最小分配单元。但在自定义内存池中,它更常来自“拆分空闲块时的向下取整”或“用户请求尺寸与块大小不匹配”。
比如空闲块 1024B,用户请求 1000B,你按需分配后剩下 24B —— 这 24B 若小于最小分配粒度(如 16B),就成内部碎片。它仍属于空闲链表,但后续可能无法被利用。
每次从空闲块
中分配
时,记录
为潜在内部碎片
但真正计入统计的,是拆分后新加入空闲链表的那部分“残余块”,且其
(如 8 或 16)
避免重复统计:同一空闲块多次拆分,只对最终不可用的尾部残余计数
不要依赖
或
算对齐开销——你的池子对齐策略由你控制,比如统一按 16B 对齐,则每块头部加 16B 元数据,这部分也属于内部碎片
为什么 std::list 遍历比 vector 快,但合并操作容易出错
因为
是双向链表,插入/删除 O(1),维持地址有序只需找到位置后 splice;而
每次插入都要 memmove,O(n)。但问题出在合并逻辑上:合并两个空闲块,不仅要删节点,还要确保前后指针正确,尤其当三个块 A-B-C 全部连续时,一次只合并 A+B,B+C 就会失效。
释放内存时,先查前驱:是否存在块
满足
再查后继:是否存在块
满足
合并顺序必须是:先和前驱合并(更新新块地址),再用新块地址判断是否能和后继合并
用
删除节点后,迭代器立即失效,别存着下一轮用;改用
或
前先保存下一个
调试时打印链表:写个
,输出每个
和
,肉眼一看就能发现断点或重叠
最易忽略的是:碎片统计必须在稳定状态下做(即所有分配/释放完成之后),中间过程的临时碎片没有意义。还有,地址比较必须用
转换,别直接比
——某些平台下行为未定义。
std::listaddrsizechar heap[1024 * 1024]std::listFreeBlock = { void* addr; size_t size; }addr = heapsize = sizeof(heap)addraddr+sizeaddrstd::vectorstd::liststd::setabgap = b.addr - (a.addr + a.size)gap > 0gap < 16freq_sizef.size - req_sizesize < MIN_ALLOC_SIZEsizeof(size_t)alignof(max_align_t)std::liststd::vectorpp.addr + p.size == freed_block.addrnfreed_block.addr + freed_block.size == n.addrstd::list::erase()std::next(it)it++dump_freelist()addrsizeuintptr_tvoid*