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

空间配置器

简化版的空间配置器

#include <cstdlib>   // 用于 malloc/free
#include <cstddef>   // 用于 size_t/ptrdiff_t
#include <new>       // 用于 placement new(构造对象)
#include <iostream>  // 仅用于测试// --------------- 1. 空闲内存块结构体 ---------------
// 用于串联空闲内存块,形成空闲链表
struct FreeObj {FreeObj* next;  // 指向下一个空闲块的指针
};// --------------- 2. 空间配置器模板类 ---------------
// 模板参数 T:配置器管理的对象类型
template <typename T>
class SimpleAllocator {
public:// ---------------- 类型定义(符合STL规范) ----------------typedef T               value_type;    // 配置器管理的对象类型typedef T*              pointer;       // 指向对象的指针typedef const T*        const_pointer; // 指向常量对象的指针typedef T&              reference;     // 对象的引用typedef const T&        const_reference; // 常量对象的引用typedef size_t          size_type;     // 内存大小/对象个数类型typedef ptrdiff_t       difference_type; // 指针差值类型// ---------------- 模板rebind(适配其他类型) ----------------// 作用:将当前配置器适配到新类型 U(如vector<int>需要为迭代器分配内存时使用)template <typename U>struct rebind {typedef SimpleAllocator<U> other;};// ---------------- 构造/析构函数(无状态,仅占位) ----------------SimpleAllocator() noexcept {}SimpleAllocator(const SimpleAllocator&) noexcept {}// 跨类型拷贝构造(如从SimpleAllocator<int>构造SimpleAllocator<double>)template <typename U>SimpleAllocator(const SimpleAllocator<U>&) noexcept {}~SimpleAllocator() noexcept {}// ---------------- 核心功能1:内存分配(allocate) ----------------// 功能:分配能存储n个T类型对象的内存(仅分配,不构造对象)// 参数n:对象个数;hint:内存分配提示(可选,此处未使用)// 返回值:指向分配内存的指针(失败返回nullptr)pointer allocate(size_type n, const void* hint = nullptr) {// 计算需要分配的总字节数size_type bytes = n * sizeof(T);pointer ret = nullptr;// 分两种情况:大内存直接用malloc,小内存用内存池if (bytes > _MAX_SMALL_BYTES) {// 1. 大内存(超过阈值):直接调用mallocret = reinterpret_cast<pointer>(std::malloc(bytes)); // 修复:void*→pointer用reinterpret_castif (ret == nullptr) {throw std::bad_alloc(); // 分配失败抛出异常(符合C++标准)}} else {// 2. 小内存(不超过阈值):从内存池的空闲链表分配// 2.1 计算当前字节数对应的空闲链表索引size_type index = _get_freelist_index(bytes);// 2.2 获取对应空闲链表的头指针FreeObj** freelist = &_free_lists[index];// 2.3 尝试从链表头部取一块内存FreeObj* free_block = *freelist;if (free_block == nullptr) {// 2.3.1 空闲链表为空:调用refill填充链表(批量分配内存块)ret = reinterpret_cast<pointer>(_refill(_round_up(bytes))); // 修复:void*→pointer用reinterpret_cast} else {// 2.3.2 空闲链表非空:取下链表头,更新链表*freelist = free_block->next; // 链表头后移ret = reinterpret_cast<pointer>(free_block); // 修复:FreeObj*→pointer用reinterpret_cast}}return ret;}// ---------------- 核心功能2:内存释放(deallocate) ----------------// 功能:释放之前allocate分配的内存(仅释放,不析构对象)// 参数p:待释放内存的指针;n:当初分配的对象个数(用于计算字节数)void deallocate(pointer p, size_type n) {if (p == nullptr) return; // 空指针直接返回,避免无效操作// 计算需要释放的总字节数size_type bytes = n * sizeof(T);if (bytes > _MAX_SMALL_BYTES) {// 1. 大内存:直接调用free(pointer→void*可隐式转换,无需cast)std::free(p);} else {// 2. 小内存:放回内存池的对应空闲链表(头插法,O(1)效率)// 2.1 计算对应空闲链表索引size_type index = _get_freelist_index(bytes);// 2.2 获取对应空闲链表头指针FreeObj** freelist = &_free_lists[index];// 2.3 将当前内存块转为FreeObj结构(用于串联链表)FreeObj* free_block = reinterpret_cast<FreeObj*>(p); // 修复:pointer→FreeObj*用reinterpret_cast// 2.4 头插法插入空闲链表free_block->next = *freelist; // 新块的next指向原链表头*freelist = free_block;       // 链表头更新为新块}}// ---------------- 辅助功能1:对象构造(construct) ----------------// 功能:在已分配的内存p上构造T类型对象(调用T的构造函数)// 参数p:内存指针;args:构造函数的参数(变长模板,支持任意参数)template <typename... Args>void construct(pointer p, Args&&... args) {// placement new:在指定内存p上构造对象,不额外分配内存::new (static_cast<void*>(p)) T(std::forward<Args>(args)...); // void*是中间类型,static_cast合法}// ---------------- 辅助功能2:对象析构(destroy) ----------------// 功能:析构p指向的T类型对象(调用T的析构函数,不释放内存)void destroy(pointer p) {p->~T(); // 显式调用析构函数(仅清理对象资源,内存需后续deallocate释放)}private:// ---------------- 内存池核心参数(静态成员,所有实例共享) ----------------static const size_type _MAX_SMALL_BYTES = 128;  // 小内存阈值(超过则视为大内存)static const size_type _ALIGN = 8;              // 内存对齐值(小内存按8字节对齐)// 空闲链表数组:大小 = (最大小内存字节数 + 对齐值 - 1) / 对齐值(覆盖所有对齐后的大小)static FreeObj* _free_lists[(_MAX_SMALL_BYTES + _ALIGN - 1) / _ALIGN];// ---------------- 辅助函数1:内存对齐(round_up) ----------------// 功能:将bytes向上对齐到_ALIGN的整数倍(确保小内存块大小统一)static size_type _round_up(size_type bytes) {// 对齐公式:(bytes + 对齐值 - 1) & ~(对齐值 - 1)(清除低3位,实现8字节对齐)return (bytes + _ALIGN - 1) & ~(_ALIGN - 1);}// ---------------- 辅助函数2:计算空闲链表索引(_get_freelist_index) ----------------// 功能:根据对齐后的字节数,计算对应的空闲链表数组下标static size_type _get_freelist_index(size_type bytes) {// 先对齐,再计算索引(例:8字节→0,16字节→1,...,128字节→15)return (_round_up(bytes) / _ALIGN) - 1;}// ---------------- 辅助函数3:填充空闲链表(_refill) ----------------// 功能:当空闲链表为空时,批量分配内存块并填充链表// 参数align_bytes:对齐后的单个内存块字节数// 返回值:其中一个内存块(供本次分配使用)static void* _refill(size_type align_bytes) {const size_type _BATCH_SIZE = 20; // 批量分配的块数(一次分配20个,减少malloc调用)// 计算批量分配的总字节数size_type total_bytes = align_bytes * _BATCH_SIZE;// 调用malloc批量分配内存(返回void*)void* batch_mem = std::malloc(total_bytes);if (batch_mem == nullptr) {// 批量分配失败:尝试单独分配一个块(保底策略)batch_mem = std::malloc(align_bytes);if (batch_mem == nullptr) {throw std::bad_alloc(); // 仍失败则抛出异常}// 单独分配时,无需填充链表(直接返回该块)return batch_mem;}// 批量分配成功:将大块内存分割成_BATCH_SIZE个小块,填充到对应空闲链表FreeObj** freelist = &_free_lists[_get_freelist_index(align_bytes)];FreeObj* current_block = reinterpret_cast<FreeObj*>(batch_mem); // 修复:void*→FreeObj*用reinterpret_castFreeObj* next_block = nullptr;// 1. 第1个块留给本次分配使用(返回给用户)void* ret = current_block;// 2. 从第2个块开始,串联成空闲链表(按字节偏移计算下一块地址)// 先转char*(1字节单位,支持精确偏移),再转FreeObj*current_block = reinterpret_cast<FreeObj*>(reinterpret_cast<char*>(batch_mem) + align_bytes // 修复:void*→char*用reinterpret_cast);*freelist = current_block; // 链表头指向第2个块// 循环串联剩余块(从第3个到第20个)for (size_type i = 2; i < _BATCH_SIZE; ++i) {// 计算下一块地址:当前块地址 + 单个块字节数(char*偏移后转FreeObj*)next_block = reinterpret_cast<FreeObj*>(reinterpret_cast<char*>(current_block) + align_bytes // 修复:FreeObj*→char*用reinterpret_cast);current_block->next = next_block; // 串联当前块和下一块current_block = next_block;       // 移动到下一块}current_block->next = nullptr; // 链表尾置空(避免野指针)return ret; // 返回第1个块给本次分配}
};// ---------------- 静态成员初始化(必须在类外初始化,所有实例共享) ----------------
template <typename T>
FreeObj* SimpleAllocator<T>::_free_lists[(_MAX_SMALL_BYTES + _ALIGN - 1) / _ALIGN] = {nullptr};// ---------------- 3. 测试代码 ----------------
int main() {// 测试1:分配/释放int类型内存(小内存,4字节→对齐到8字节)SimpleAllocator<int> int_alloc;int* p1 = int_alloc.allocate(1); // 分配1个int的内存int_alloc.construct(p1, 100);    // 在p1上构造int对象(值为100)std::cout << "p1 value: " << *p1 << std::endl; // 输出:p1 value: 100int_alloc.destroy(p1);           // 析构p1指向的对象int_alloc.deallocate(p1, 1);     // 释放内存(放回空闲链表)// 测试2:分配/释放string类型内存(大内存,string对象通常大于128字节)SimpleAllocator<std::string> str_alloc;std::string* p2 = str_alloc.allocate(2); // 分配2个string的内存str_alloc.construct(p2, "Hello");        // 构造第1个string(值为"Hello")str_alloc.construct(p2 + 1, "Allocator");// 构造第2个string(值为"Allocator")std::cout << "p2[0]: " << p2[0] << ", p2[1]: " << p2[1] << std::endl; // 输出:p2[0]: Hello, p2[1]: Allocatorstr_alloc.destroy(p2 + 1);               // 析构第2个stringstr_alloc.destroy(p2);                   // 析构第1个stringstr_alloc.deallocate(p2, 2);             // 释放内存(直接free)return 0;
}

核心设计说明

  1. 小内存 vs 大内存分治

    • 阈值 _MAX_SMALL_BYTES=128:小于等于 128 字节的内存视为 “小内存”,用内存池管理;超过则视为 “大内存”,直接调用 malloc/free(大内存用内存池收益低,且易浪费空间)。
    • 对齐规则 _ALIGN=8:小内存按 8 字节对齐,确保所有小内存块大小统一,方便分类存储(如 5 字节→8 字节,9 字节→16 字节)。
  2. 空闲链表数组 _free_lists

    • 本质:指针数组,每个元素指向一条 “相同大小空闲块的链表”(如索引 0→8 字节块链表,索引 1→16 字节块链表)。
    • 大小计算:(128+8-1)/8=16,共 16 条链表,覆盖 8~128 字节的所有对齐后大小。
  3. 关键辅助函数

    • _round_up:将内存大小向上对齐到 8 的整数倍,保证小内存块大小统一。
    • _get_freelist_index:根据对齐后的大小计算链表索引,快速定位空闲链表。
    • _refill:空闲链表为空时,批量分配 20 个块(减少 malloc 调用次数),1 个块返回给用户,其余串成链表备用。
  4. 对象构造 / 析构分离

    • construct:用 placement new 在已分配内存上构造对象(不额外分配内存),支持任意构造函数参数(变长模板)。
    • destroy:显式调用对象析构函数(仅清理对象资源,内存需通过 deallocate 释放)。

a. 对于 new 来说,编译器会先调用 ::operator new 分配内存;然后调用 Obj::Obj() 构造对象内容。

b. 对于 delete 来说,编译器会先调用 Obj::~Obj() 析构对象;然后调用  ::operator delete 释放空间。

为了精密分工,STL allocator 决定将这两个阶段操作区分开来。

c. 对象构造由 ::construct() 负责;对象释放由 ::destroy() 负责。

d. 内存配置由 alloc::allocate() 负责;内存释放由 alloc::deallocate() 负责;

空闲链表(自由链表)

在这段代码中,空闲链表(自由链表)是一种 “数组管理多链表” 的分层结构—— 核心是 “一个静态数组(_free_lists)+ 多组单链表”,数组的每个元素对应一条 “固定大小的空闲内存块单链表”,所有空闲内存块通过链表节点的指针串联,最终实现 “小内存块按大小分类管理、快速分配与回收”。

要理解其具体形式,需拆解两个核心组成部分:链表节点结构 和 数组与链表的关联方式,再结合内存分配 / 释放的操作逻辑,就能清晰看到空闲链表的完整形态。

一、第一步:明确空闲链表的 “节点结构”——FreeObj

所有空闲内存块,都会被 “包装” 成 FreeObj 类型的节点,这是链表串联的基础。代码中定义的 FreeObj 结构体非常简洁:

struct FreeObj {FreeObj* next;  // 唯一成员:指向“下一个空闲块节点”的指针
};
  • 本质FreeObj 是一个 “链表节点模板”—— 它不存储实际数据,仅用 next 指针将多个空闲内存块串联成单链表。
  • 内存块与节点的关联:当一块内存被释放为 “空闲块” 时,代码会将其强制转换为 FreeObj* 类型(通过 reinterpret_cast),此时这块内存的前 4 字节(32 位系统)或 8 字节(64 位系统) 会被用作 next 指针,剩余部分仍为原始内存空间(下次分配时会覆盖 next 指针,不影响使用)。

举个例子:一块 8 字节的空闲内存,作为 FreeObj 节点的形态如下(64 位系统):

内存地址范围存储内容作用
0x100 ~ 0x107FreeObj* next 指针指向链表中下一个空闲块

二、第二步:明确空闲链表的 “管理结构”——_free_lists 数组

单个链表只能管理 “同一种大小” 的空闲块,而代码需要管理 “8 字节、16 字节、24 字节……128 字节” 等多种小内存块(按 8 字节对齐),因此用 静态数组 _free_lists 来分类管理这些链表。

1. 数组的定义与大小

代码中 _free_lists 的定义是:

static FreeObj* _free_lists[(_MAX_SMALL_BYTES + _ALIGN - 1) / _ALIGN];
  • 核心参数:
    • _MAX_SMALL_BYTES = 128:内存池管理的 “最大小内存字节数”(超过则用 malloc 直接分配);
    • _ALIGN = 8:内存对齐值(所有小内存块按 8 字节向上对齐,确保块大小统一)。
  • 数组大小计算:(128 + 8 - 1) / 8 = 16 → 数组有 16 个元素,每个元素对应一条 “固定大小的空闲链表”。

2. 数组与链表的对应关系

数组的每个下标对应一种 “对齐后的空闲块大小”,下标与块大小的映射规则由 _get_freelist_index 函数定义:

static size_type _get_freelist_index(size_type bytes) {return (_round_up(bytes) / _ALIGN) - 1;
}
  • _round_up(bytes):将请求的字节数向上对齐到 8 的整数倍(如 5 字节→8 字节,9 字节→16 字节);

  • 映射示例(数组下标 → 对应空闲块大小):

    数组下标对齐后的块大小管理的请求内存范围(未对齐)
    08 字节1~8 字节
    116 字节9~16 字节
    224 字节17~24 字节
    .........
    15128 字节121~128 字节
  • 数组元素的作用:每个元素是一条 “对应大小空闲链表” 的头指针—— 指向链表的第一个空闲块节点;若链表为空,则头指针为 nullptr

三、第三步:空闲链表的 “实际形态”(结合操作逻辑)

空闲链表的形态不是固定的,会随着 allocate(分配)和 deallocate(释放)动态变化,我们通过两个核心场景来还原其形态:

场景 1:初始状态(无空闲块)

程序启动时,_free_lists 数组的所有元素都被初始化为 nullptr(类外静态初始化),此时所有空闲链表均为空:

_free_lists[0] → nullptr  (8字节链表空)
_free_lists[1] → nullptr  (16字节链表空)
...
_free_lists[15] → nullptr (128字节链表空)

场景 2:第一次分配 8 字节内存(触发_refill 填充链表)

当调用 int_alloc.allocate(1)(int 占 4 字节,对齐后 8 字节)时:

  1. 计算下标:_get_freelist_index(4) → 0 → 操作 _free_lists[0](8 字节链表);
  2. 发现 _free_lists[0] 为 nullptr(链表空),调用 _refill(8) 批量分配空闲块;
  3. _refill 逻辑:
    • 一次分配 20个8字节块_BATCH_SIZE=20),总字节数 20*8=160 字节;
    • 将第 1 个块返回给用户(作为分配结果);
    • 剩余 19 个块串联成单链表,头指针赋值给 _free_lists[0]

此时 _free_lists[0] 对应的 8 字节链表形态如下:

_free_lists[0] → 块2(8字节) → 块3(8字节) → 块4(8字节) → ... → 块20(8字节) → nullptr
  • 块 2~ 块 20:19 个空闲块,通过 FreeObj::next 指针串联;
  • 每个块的 next 指针指向 “下一个空闲块”,最后一个块的 next 为 nullptr(链表尾)。

场景 3:释放 8 字节内存(头插法回链表)

当用户释放之前分配的 8 字节块(块 1)时,调用 int_alloc.deallocate(p1, 1)

  1. 计算下标:仍为 0 → 操作 _free_lists[0]
  2. 将块 1 转换为 FreeObj* 类型,用头插法插入链表:
    • 块 1 的 next 指针指向原链表头(块 2);
    • 更新 _free_lists[0] 为块 1(新链表头)。

此时 8 字节链表形态更新为:

_free_lists[0] → 块1(8字节) → 块2(8字节) → 块3(8字节) → ... → 块20(8字节) → nullptr
  • 头插法的优势:O (1) 时间复杂度,无需遍历链表,释放效率极高。

场景 4:再次分配 8 字节内存(从链表头取块)

当再次分配 8 字节内存时:

  1. 直接取 _free_lists[0] 指向的块 1(链表头);
  2. 更新 _free_lists[0] 为块 2(链表头后移);
  3. 将块 1 返回给用户(此时块 1 的 next 指针会被覆盖,不影响使用)。

此时链表形态恢复为场景 2 的状态:

_free_lists[0] → 块2(8字节) → 块3(8字节) → ... → 块20(8字节) → nullptr

四、总结:空闲链表的整体形式

这段代码中的空闲链表,本质是一种 “数组索引 + 单链表” 的分层结构,可概括为:

  1. 顶层:_free_lists 静态数组:作为 “大小分类目录”,16 个元素对应 16 种对齐后的小内存块大小(8~128 字节);
  2. 底层:多条单链表:数组的每个元素对应一条单链表,链表节点是 FreeObj 类型的空闲内存块,通过 next 指针串联;
  3. 核心特性
    • 按大小分类管理:避免不同大小的空闲块混放,分配时直接定位对应链表,效率高;
    • 头插 / 头取:分配和释放均为 O (1) 时间复杂度,适合小内存频繁操作;
    • 批量填充:链表空时通过 _refill 批量分配块,减少 malloc 调用次数(降低系统开销)。

简单说,这个空闲链表就像 “多个分类抽屉”—— 数组是抽屉的标签(8 字节、16 字节……),每个抽屉里放着一串相同大小的空闲块,取 / 放块时直接打开对应抽屉,从最上面拿 / 放,高效且有序。

为了更深入理解 SimpleAllocator 空闲链表的工作机制,我们可以结合 多类型内存交替分配 / 释放、内存碎片产生、大 / 小内存混合操作、对象生命周期管理 等复杂场景展开分析。每个场景会拆解操作步骤,并展示空闲链表的动态变化,同时指出代码的优势与局限性。

场景 1:多类型小内存交替分配与释放(核心体现 “分类管理” 优势)

场景描述

同时分配 3 种不同大小的小内存对象(均≤128 字节),交替释放后观察空闲链表的回收逻辑,重点体现 “不同大小的块归位到对应链表” 的特性。
涉及对象:

  • char(1 字节→对齐 8 字节,对应 _free_lists[0]);
  • short(2 字节→对齐 8 字节,对应 _free_lists[0]);
  • 自定义小对象 SmallObj(12 字节→对齐 16 字节,对应 _free_lists[1])。

操作步骤与空闲链表变化

步骤 1:初始状态(所有链表为空)

_free_lists[0](8字节):nullptr  
_free_lists[1](16字节):nullptr  
_free_lists[2~15]:均为nullptr  

步骤 2:分配 1 个char(8 字节,触发_refill

  • 计算下标:_get_freelist_index(1) → 0(8 字节链表);
  • 链表为空,调用 _refill(8):批量分配 20 个 8 字节块(总 160 字节),第 1 块返回用户,剩余 19 块串联成链表。
    此时空闲链表状态:
_free_lists[0] → 块2(8B)→ 块3(8B)→ ... → 块20(8B)→ nullptr  
_free_lists[1]:nullptr  

用户拿到的内存:char* c1 = alloc.allocate(1)(块 1)。

步骤 3:分配 1 个SmallObj(16 字节,触发_refill

  • 计算下标:_get_freelist_index(12) → 1(16 字节链表);
  • 链表为空,调用 _refill(16):批量分配 20 个 16 字节块(总 320 字节),第 1 块返回用户,剩余 19 块串联成链表。
    此时空闲链表状态:
_free_lists[0] → 块2(8B)→ ... → 块20(8B)→ nullptr  
_free_lists[1] → 块2(16B)→ ... → 块20(16B)→ nullptr  

用户拿到的内存:SmallObj* obj1 = alloc.allocate(1)(块 1,16B)。

步骤 4:分配 1 个short(8 字节,直接从链表取)

  • 下标 0,_free_lists[0] 非空,取链表头(块 2)返回用户,链表头后移到块 3。
    此时空闲链表状态:
_free_lists[0] → 块3(8B)→ ... → 块20(8B)→ nullptr  
_free_lists[1] → 块2(16B)→ ... → 块20(16B)→ nullptr  

用户拿到的内存:short* s1 = alloc.allocate(1)(块 2,8B)。

步骤 5:释放char* c1(8 字节,头插回链表 0)

  • 将块 1(8B)转为FreeObj*,头插法插入 _free_lists[0],新链表头为块 1。
    此时空闲链表状态:
_free_lists[0] → 块1(8B)→ 块3(8B)→ ... → 块20(8B)→ nullptr  
_free_lists[1] → 块2(16B)→ ... → 块20(16B)→ nullptr  

步骤 6:释放SmallObj* obj1(16 字节,头插回链表 1)

  • 将块 1(16B)转为FreeObj*,头插法插入 _free_lists[1],新链表头为块 1。
    此时空闲链表状态:
_free_lists[0] → 块1(8B)→ 块3(8B)→ ... → 块20(8B)→ nullptr  
_free_lists[1] → 块1(16B)→ 块2(16B)→ ... → 块20(16B)→ nullptr  

核心结论

  • 空闲链表的 “分类管理” 生效:8 字节块只在 _free_lists[0] 流转,16 字节块只在 _free_lists[1] 流转,互不干扰;
  • 分配 / 释放效率极高:小内存分配优先从对应链表取(O (1)),释放时头插回链表(O (1)),无需遍历所有块。

场景 2:小内存频繁分配释放导致的 “内存碎片”(暴露局限性)

场景描述

频繁分配 / 释放 不同大小的小内存块,观察 “内部碎片” 和 “外部碎片” 的产生过程,以及空闲链表无法合并碎片的问题。
涉及对象:

  • int(4 字节→8B,链表 0);
  • SmallObj2(20 字节→24B,链表 2:24B/8=3 → 下标 3-1=2)。

操作步骤与碎片产生

步骤 1:批量分配 8B 和 24B 块

  • 分配 5 个int(8B):从链表 0 取 5 块(块 1~5),链表 0 剩余 15 块;
  • 分配 3 个SmallObj2(24B):从链表 2 取 3 块(块 1~3),链表 2 剩余 17 块。
    此时内存使用状态:
8B块:块1~5被占用,块6~20空闲(链表0);
24B块:块1~3被占用,块4~20空闲(链表2);

步骤 2:交替释放部分块(产生 “外部碎片”)

  • 释放int*块 2、4(8B):回插链表 0,链表 0 变为「块 4→块 2→块 6→...→块 20」;
  • 释放SmallObj2*块 1、3(24B):回插链表 2,链表 2 变为「块 3→块 1→块 4→...→块 20」。

此时空闲链表中存在 “不连续的空闲块”,但由于 不同链表的块大小固定,无法合并

  • 链表 0 的空闲块都是 8B,即使相邻也无法合并成 16B 或更大块;
  • 链表 2 的空闲块都是 24B,同样无法合并成更大块。

步骤 3:请求分配 32B 内存(触发新链表,但无法利用现有碎片)

  • 用户请求 32B 内存(对齐后 32B,对应链表 3:32/8-1=3);
  • 链表 3 为空,调用_refill(32)分配 20 个 32B 块,无法利用链表 0/2 的 8B/24B 空闲块(大小不匹配)。

此时产生 “外部碎片”:链表 0/2 有大量空闲块,但无法满足 32B 的请求,只能重新分配新内存。

核心结论

  • 该空闲链表的 “固定大小块” 设计会导致 外部碎片:不同链表的空闲块无法合并,即使总空闲内存足够,也无法满足其他大小的请求;
  • 同时存在 内部碎片:比如请求 5 字节内存,实际分配 8 字节,浪费 3 字节(对齐导致)。

场景 3:大内存与小内存混合分配释放(体现 “分层策略”)

场景描述

同时分配 小内存(≤128B)和大内存(>128B),观察两者的独立处理逻辑,以及空闲链表与直接malloc/free的协作。
涉及对象:

  • int(4B→8B,小内存,链表 0);
  • std::string(假设存储长字符串,对象大小 > 128B,大内存);
  • double(8B→8B,小内存,链表 0)。

操作步骤与处理逻辑

步骤 1:分配小内存int(8B)

  • 走空闲链表 0:首次分配触发_refill(8),获取块 1,链表 0 剩余 19 块。

步骤 2:分配大内存std::string(>128B)

  • 计算字节数:假设std::string对象 + 数据总大小 256B,超过_MAX_SMALL_BYTES=128
  • 直接调用malloc(256)分配,不经过空闲链表;
  • 用户拿到内存:std::string* str1 = str_alloc.allocate(1)

步骤 3:分配小内存double(8B)

  • 走空闲链表 0:从链表头取块 2,链表 0 剩余 18 块。

步骤 4:释放小内存int(8B)

  • 回插链表 0:头插法插入块 1,链表 0 变为「块 1→块 3→...→块 20」。

步骤 5:释放大内存std::string(256B)

  • 直接调用free(str1)释放,不经过空闲链表;
  • 大内存的释放与空闲链表完全独立,互不影响。

步骤 6:再次分配小内存int(8B)

  • 从链表 0 取头块 1(刚释放的块),链表 0 变为「块 3→...→块 20」。

核心结论

  • 代码的 “分层策略” 清晰:小内存走空闲链表(复用内存,减少malloc),大内存直接malloc/free(避免为大块分配维护链表,节省内存);
  • 两者独立工作,大内存的分配释放不会干扰空闲链表的状态,反之亦然。

场景 4:对象析构与内存回收的 “分离逻辑”(避免资源泄漏)

场景描述

分配 带资源的自定义对象(如持有动态内存的MyString),验证SimpleAllocatorconstruct/destroyallocate/deallocate的配合 —— 必须先析构对象(释放内部资源),再释放内存(回链表),否则会导致资源泄漏。
自定义对象:

class MyString {
private:char* _data;size_t _len;
public:MyString(const char* s) {_len = strlen(s);_data = new char[_len + 1]; // 内部动态分配内存strcpy(_data, s);}~MyString() {delete[] _data; // 析构时释放内部内存}const char* c_str() const { return _data; }
};

操作步骤与风险点

步骤 1:分配MyString内存(小内存,16B:char*+size_t→对齐 16B,链表 1)

  • 调用allocate(1):从链表 1 取块 1,返回MyString* p
  • 此时p指向的内存是 “原始内存”,未构造对象,_data未初始化。

步骤 2:构造MyString对象(调用construct

  • 调用alloc.construct(p, "Hello Allocator"):通过 placement new 调用MyString构造函数;
  • 构造后,p->_data指向堆内存(存储字符串),对象资源初始化完成。

步骤 3:直接释放内存(不析构对象,风险!)

  • 错误操作:alloc.deallocate(p, 1)(未调用destroy);
  • 后果:p指向的内存回插链表 1,但p->_data指向的堆内存未释放(内存泄漏);
  • 后续分配该块内存时,新对象的构造会覆盖_data指针,泄漏的内存永远无法回收。

步骤 4:正确操作(先析构,再释放)

  • 分配新对象:MyString* p2 = alloc.allocate(1)
  • 构造:alloc.construct(p2, "Correct Usage")
  • 析构:alloc.destroy(p2)(调用~MyString(),释放_data指向的内存);
  • 释放内存:alloc.deallocate(p2, 1)(内存回插链表 1,无资源泄漏)。

核心结论

  • SimpleAllocator 严格区分 “对象构造 / 析构” 和 “内存分配 / 释放”:
    • construct/destroy 负责对象的生命周期(调用构造 / 析构函数,管理内部资源);
    • allocate/deallocate 负责内存的生命周期(分配 / 释放原始内存,与对象资源无关);
  • 必须遵循 “先析构,再释放” 的顺序,否则会导致对象内部资源泄漏(这是所有 STL 风格分配器的通用规则)。

总结:复杂场景下的空闲链表特性

场景空闲链表表现代码优势代码局限性
多类型小内存交替操作按大小分类管理,独立链表流转分配 / 释放效率 O (1),无类型干扰
频繁分配释放产生外部碎片(不同链表块无法合并)批量分配减少malloc次数无法处理碎片,内存利用率可能下降
大 / 小内存混合操作小内存走链表,大内存直接malloc分层策略平衡效率与内存开销大内存无复用,依赖系统malloc
对象生命周期管理内存回插链表前需手动析构对象分离对象与内存管理,灵活度高需用户保证 “先析构再释放”,易出错

这些场景也体现了工业级内存池(如 tcmalloc、jemalloc)的优化方向:比如支持空闲块合并(解决外部碎片)、线程局部缓存(减少锁竞争)、动态调整批量分配大小(平衡内存利用率与效率)等,而SimpleAllocator作为简化版本,仅实现了核心的空闲链表管理,适合理解内存池的基础原理。

http://www.xdnf.cn/news/1479673.html

相关文章:

  • 【STM32HAL-----NRF24L01】
  • leetcode LCR 159 库存管理III
  • Qt网络通信服务端与客户端学习
  • 第5章递归:分治法
  • Qt文字滚动效果学习
  • MySQL 高可用方案之 MHA 架构搭建与实践
  • 常用配置文件
  • 去中心化投票系统开发教程 第三章:智能合约设计与开发
  • [网络入侵AI检测] docs | 任务二分类与多分类
  • 算法题-链表03
  • react native 出现 FATAL EXCEPTION: OkHttp Dispatcher
  • LeetCode 2841.几乎唯一子数组的最大和
  • AI智能体架构全流程全解析
  • [光学原理与应用-432]:非线性光学 - 既然光也是电磁波,为什么不能直接通过电生成特定频率的光波?
  • 打造一款高稳定、低延迟、跨平台RTSP播放器的技术实践
  • 基于FPGA的电梯控制系统设计(论文+源码)
  • 动态内存分配
  • DeepSeek辅助在64位Linux中编译运行32位的asm-xml-1.4程序
  • Day22_【机器学习—集成学习(1)—基本思想、分类】
  • leetcode 215 数组中的第K个最大元素
  • Jupyter Notebook与cpolar:构建跨地域数据科学协作平台
  • 正态分布 - 计算 Z-Score 的 无偏估计
  • 计算机主板上的那颗纽扣电池的作用是什么?
  • OSG中TerrainManipulator(地形适配操纵器)
  • STM32CubeProgrammer软件安装
  • Qt 中的 Q_OBJECT 宏详解 —— 从源码到底层机制的全面剖析
  • 2023年ASOC SCI2区TOP,改进元启发式算法+考虑医护人员技能水平的家庭健康护理路径规划,深度解析+性能实测
  • 【Redis】缓存的穿透、击穿和雪崩
  • 一个正常的 CSDN 博客账号,需要做哪些基础准备?
  • C++基础知识