空间配置器
简化版的空间配置器
#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;
}
核心设计说明
小内存 vs 大内存分治
- 阈值
_MAX_SMALL_BYTES=128
:小于等于 128 字节的内存视为 “小内存”,用内存池管理;超过则视为 “大内存”,直接调用malloc/free
(大内存用内存池收益低,且易浪费空间)。 - 对齐规则
_ALIGN=8
:小内存按 8 字节对齐,确保所有小内存块大小统一,方便分类存储(如 5 字节→8 字节,9 字节→16 字节)。
- 阈值
空闲链表数组
_free_lists
- 本质:指针数组,每个元素指向一条 “相同大小空闲块的链表”(如索引 0→8 字节块链表,索引 1→16 字节块链表)。
- 大小计算:
(128+8-1)/8=16
,共 16 条链表,覆盖 8~128 字节的所有对齐后大小。
关键辅助函数
_round_up
:将内存大小向上对齐到 8 的整数倍,保证小内存块大小统一。_get_freelist_index
:根据对齐后的大小计算链表索引,快速定位空闲链表。_refill
:空闲链表为空时,批量分配 20 个块(减少malloc
调用次数),1 个块返回给用户,其余串成链表备用。
对象构造 / 析构分离
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 ~ 0x107 | FreeObj* 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 字节);映射示例(数组下标 → 对应空闲块大小):
数组下标 对齐后的块大小 管理的请求内存范围(未对齐) 0 8 字节 1~8 字节 1 16 字节 9~16 字节 2 24 字节 17~24 字节 ... ... ... 15 128 字节 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 字节)时:
- 计算下标:
_get_freelist_index(4) → 0
→ 操作_free_lists[0]
(8 字节链表); - 发现
_free_lists[0]
为nullptr
(链表空),调用_refill(8)
批量分配空闲块; _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)
:
- 计算下标:仍为 0 → 操作
_free_lists[0]
; - 将块 1 转换为
FreeObj*
类型,用头插法插入链表:- 块 1 的
next
指针指向原链表头(块 2); - 更新
_free_lists[0]
为块 1(新链表头)。
- 块 1 的
此时 8 字节链表形态更新为:
_free_lists[0] → 块1(8字节) → 块2(8字节) → 块3(8字节) → ... → 块20(8字节) → nullptr
- 头插法的优势:O (1) 时间复杂度,无需遍历链表,释放效率极高。
场景 4:再次分配 8 字节内存(从链表头取块)
当再次分配 8 字节内存时:
- 直接取
_free_lists[0]
指向的块 1(链表头); - 更新
_free_lists[0]
为块 2(链表头后移); - 将块 1 返回给用户(此时块 1 的
next
指针会被覆盖,不影响使用)。
此时链表形态恢复为场景 2 的状态:
_free_lists[0] → 块2(8字节) → 块3(8字节) → ... → 块20(8字节) → nullptr
四、总结:空闲链表的整体形式
这段代码中的空闲链表,本质是一种 “数组索引 + 单链表” 的分层结构,可概括为:
- 顶层:_free_lists 静态数组:作为 “大小分类目录”,16 个元素对应 16 种对齐后的小内存块大小(8~128 字节);
- 底层:多条单链表:数组的每个元素对应一条单链表,链表节点是
FreeObj
类型的空闲内存块,通过next
指针串联; - 核心特性:
- 按大小分类管理:避免不同大小的空闲块混放,分配时直接定位对应链表,效率高;
- 头插 / 头取:分配和释放均为 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
),验证SimpleAllocator
的construct/destroy
与allocate/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
作为简化版本,仅实现了核心的空闲链表管理,适合理解内存池的基础原理。