内存池的核心逻辑是预先分配连续内存并按固定大小切分为槽位,用裸指针在每块前sizeof(void*)字节构建空闲链表,分配时取头节点、释放时插回头部,需手动处理对齐、构造/析构及线程安全。

内存池的核心逻辑是什么
内存池不是某种标准库设施,而是手动管理一块连续内存区域,避免频繁调用 new / delete。关键在于:预先分配一大块 char 数组(或 std::aligned_storage_t),按固定大小切分成若干“槽位”,每个槽位通过指针链表串起来形成空闲链;分配时取头节点,释放时插回头部。
如何用裸指针构建空闲链表
不能直接用 void* 存储链接信息——对象析构后内存仍需保存下一个空闲块地址。常见做法是把每块内存的前 sizeof(void*) 字节当作指针域:
// 假设块大小为 64 字节,对齐要求 8 字节
char* pool = new char[pool_size];
void** free_list = nullptr;
<p>// 初始化:将整块内存切成链表
for (size_t i = 0; i < pool_size; i += block_size) {
void<strong> next_ptr = reinterpret_cast<void</strong>>(pool + i);
*next_ptr = free_list;
free_list = next_ptr;
}注意:free_list 是 void** 类型,指向的是“当前空闲块开头处存着的下一个空闲块地址”,不是块本身地址。分配时返回 static_cast<void>(free_list + 1)</void>,释放时写回 *static_cast<void>(ptr) = free_list</void>。
- 必须确保
block_size >= sizeof(void*),否则无法存指针 - 若对象有对齐要求(如
alignof(double)),需向上对齐块大小,并用std::align调整起始地址 - 不能在释放后的内存上调用
operator delete—— 整个池生命周期内只调用一次delete[] pool
如何支持不同类型的对象构造/析构
裸指针内存池不自动调用构造函数。分配后需显式 placement new,释放前需显式调用析构函数:
立即学习“C++免费学习笔记(深入)”;
template<typename T>
T* allocate() {
if (!free_list) return nullptr;
void* ptr = static_cast<void*>(free_list + 1);
free_list = *free_list;
return new(ptr) T(); // placement new
}
<p>template<typename T>
void deallocate(T<em> obj) {
if (!obj) return;
obj->~T(); // 显式析构
void<strong> next = reinterpret_cast<void</strong>>(obj);
</em>next = free_list;
free_list = next;
}这里 free_list 指向的是“能存指针的位置”,所以 static_cast<void>(free_list + 1)</void> 才是真正给用户用的对象起始地址。
- 若类型
T有非平凡析构函数,deallocate必须调用~T(),否则资源泄漏 - 若类型无默认构造函数,
allocate需接受参数并转发给 placement new - 不要对
obj直接做delete obj—— 它不是new出来的
为什么 std::pmr::memory_resource 更值得优先考虑
C++17 引入了 std::pmr::polymorphic_allocator 和配套的 std::pmr::monotonic_buffer_resource、std::pmr::synchronized_pool_resource。它们封装了上述所有细节,且线程安全(后者)、支持嵌套、可组合:
std::pmr::synchronized_pool_resource pool; std::pmr::vector<int> v(&pool); v.resize(1000); // 内存来自池,析构时自动归还
自己手写指针内存池容易漏掉边界检查、对齐处理、多线程竞争、异常安全(比如 placement new 抛异常后需回滚 free_list)等问题。除非有极致性能要求或嵌入式限制,否则优先用 std::pmr。
真正要手写时,最容易被忽略的是:释放对象后没清零指针域,导致后续分配拿到脏数据;或者块大小没对齐,引发未定义行为;还有就是忘记在析构函数里遍历所有已分配对象调用析构 —— 这些都得靠人工保证,没有编译器帮你兜底。


















