《C++——定长内存池》

发布于:2025-09-05 ⋅ 阅读:(16) ⋅ 点赞:(0)

一、为什么需要内存池?

常规的new/delete操作存在两个主要问题:

  • 性能开销大:每次new都需要向操作系统申请内存,delete需要归还给系统,这涉及内核态与用户态的切换,在高频次调用时性能损耗明显。
  • 内存碎片:频繁分配和释放大小不一的内存块,会导致内存空间被分割成大量不连续的小块,形成碎片,降低内存利用率。

内存池的核心思想是 “预分配、再利用”:提前向系统申请一块大块内存,然后由内存池自主管理这块内存的分配与回收,避免频繁与操作系统交互,同时减少碎片。

二、定长内存池的设计思路

定长内存池专门用于管理固定大小的对象(如链表节点、树节点等),其设计围绕两个核心组件:

  1. 内存块预分配:一次性申请大块内存,避免频繁系统调用。
  2. 自由链表(Free List):维护已释放的对象内存块,实现内存复用。

具体工作流程:

  1. 当需要分配对象时,先检查自由链表是否有可用内存块,若有则直接复用;
  2. 若自由链表为空,则从预分配的大块内存中分割出一块新内存;
  3. 当释放对象时,不直接归还给系统,而是将其加入自由链表,等待下次复用。

整理的思路图:
在这里插入图片描述

三、定长内存池的代码实现

以下是一个基于模板的定长内存池实现,支持任意类型的对象管理:

#include <iostream>
#include <vector>
#include <time.h>
using std::cout;
using std::endl;

#ifdef _WIN32
#include<windows.h>
#else
// 
#endif

// 跨平台内存分配(Windows/Linux)
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
    // Windows使用VirtualAlloc分配内存(按页分配,1页=4KB)
    void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
    // Linux可使用brk或mmap
    void* ptr = mmap(nullptr, kpage * 4096, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
#endif
    if (ptr == nullptr)
        throw std::bad_alloc();
    return ptr;
}

template<class T>
class ObjectPool
{
public:
    // 分配对象
    T* New()
    {
        T* obj = nullptr;

        // 1. 优先从自由链表获取内存
        if (_freeList)
        {
            void* next = *((void**)_freeList); // 取自由链表下一个节点
            obj = (T*)_freeList;
            _freeList = next; // 移动自由链表头
        }
        else
        {
            // 2. 自由链表为空,从预分配内存中分割
            if (_remainBytes < sizeof(T))
            {
                // 预分配128KB内存(可根据需求调整)
                _remainBytes = 128 * 1024;
                _memory = (char*)SystemAlloc(_remainBytes >> 13); // 按页计算(128KB=32页)
                if (_memory == nullptr)
                {
                    throw std::bad_alloc();
                }
            }

            // 分割内存(若对象大小小于指针,按指针大小分配,保证链表节点能存储地址)
            obj = (T*)_memory;
            size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
            _memory += objSize;
            _remainBytes -= objSize;
        }

        // 3. 调用对象构造函数(placement new)
        new(obj)T;

        return obj;
    }

    // 释放对象
    void Delete(T* obj)
    {
        // 1. 调用对象析构函数
        obj->~T();

        // 2. 将内存块加入自由链表(头插法)
        *(void**)obj = _freeList; // 存储下一个自由节点地址
        _freeList = obj; // 更新自由链表头
    }

private:
    char* _memory = nullptr;       // 指向当前未分配的大块内存
    size_t _remainBytes = 0;       // 大块内存中剩余的字节数
    void* _freeList = nullptr;     // 自由链表头(存储可复用的内存块)
};

四、核心细节解析

1 SystemAlloc

// 跨平台内存分配(Windows/Linux)
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
    // Windows使用VirtualAlloc分配内存(按页分配,1页=4KB)
    void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
    // Linux可使用brk或mmap
    void* ptr = mmap(nullptr, kpage * 4096, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
#endif
    if (ptr == nullptr)
        throw std::bad_alloc();
    return ptr;
}

这段代码是一个跨平台的内存分配函数,用于向操作系统直接申请内存(绕过 C 标准库的malloc),是定长内存池实现中 “预分配大块内存” 的底层支持

2 New()

T* New()
    {
        T* obj = nullptr;

        // 1. 优先从自由链表获取内存
        if (_freeList)
        {
            void* next = *((void**)_freeList); // 取自由链表下一个节点
            obj = (T*)_freeList;
            _freeList = next; // 移动自由链表头
        }
        else
        {
            // 2. 自由链表为空,从预分配内存中分割
            if (_remainBytes < sizeof(T))
            {
                // 预分配128KB内存(可根据需求调整)
                _remainBytes = 128 * 1024;
                _memory = (char*)SystemAlloc(_remainBytes >> 13); // 按页计算(128KB=32页)
                if (_memory == nullptr)
                {
                    throw std::bad_alloc();
                }
            }

            // 分割内存(若对象大小小于指针,按指针大小分配,保证链表节点能存储地址)
            obj = (T*)_memory;
            size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
            _memory += objSize;
            _remainBytes -= objSize;
        }

        // 3. 调用对象构造函数(placement new)
        new(obj)T;

        return obj;
    }

这段 T* New() 函数是定长内存池的核心分配逻辑,负责高效地为 T 类型对象分配内存。它的设计巧妙之处在于优先复用已释放的内存(通过自由链表),仅在必要时从预分配的大块内存中分割新空间,从而避免频繁的系统调用。下面逐行解析其核心逻辑:

函数的核心目标是:用尽可能低的开销(避免系统调用)为 T 类型对象分配一块内存,并调用其构造函数

流程分为三步:

  1. 优先从「自由链表」中获取可复用的内存块;
  2. 若自由链表为空,从预分配的大块内存中分割新内存;
  3. 在分配的内存上调用 T 的构造函数
if (_freeList)
{
    void* next = *((void**)_freeList); // 取自由链表下一个节点
    obj = (T*)_freeList;
    _freeList = next; // 移动自由链表头
}

自由链表(_freeList):本质是一个单链表,存储所有已释放的内存块(这些内存块曾用于存储 T 类型对象,现在可复用)。

操作逻辑:
从链表头取一个节点(_freeList)作为当前要分配的 obj,然后将链表头更新为下一个节点(_freeList = next),完成复用。自由链表为空时,从预分配内存中分割

((void**)_freeList)
  • 直接利用obj自身的内存空间的前几个字节,
  • 存储 “下一个空闲对象的地址”(即链表的 “指针域”)。

为什么用 void** 转换?

  1. void* 不能被解引用(无法直接访问它指向的内存内容),而且不能直接给void* 指向的内存赋值(因为编译器不知道这块内存的类型和大小)。
  2. 自由链表的每个节点(内存块)需要存储「下一个节点的地址」(才能形成链表)。因此,每个内存块的起始位置会被当作一个 void* 指针,用于存放链表的「下一个节点地址」。
  3. (void**)_freeList 表示:将当前内存块的起始地址看作一个 “指向指针的指针”,通过 ((void*)_freeList) 即可取出下一个节点的地址。

在这里插入图片描述

else
{
    // 若剩余内存不足,重新申请大块内存
    if (_remainBytes < sizeof(T))
    {
		_remainBytes = 128 * 1024;
		//_memory = (char*)malloc(_remainBytes);
		_memory = (char*)SystemAlloc(_remainBytes >> 13);
		//等价于 _remainBytes / 8192(因为1页=8KB=2^13字节)
		//计算结果为16(128KB / 8KB = 16页),即申请16页内存
		if (_memory == nullptr)
		{
			throw std::bad_alloc();
		}
    }

    // 分割内存块
    obj = (T*)_memory;
    // 若对象大小 < 指针大小,按指针大小分配(保证能存下链表节点地址)
    size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
    _memory += objSize; // 移动内存指针到下一个可用位置
    _remainBytes -= objSize; // 减少剩余内存计数
}

if里很好理解,都是为了申请内存

分割内存的细节:
为什么要比较 sizeof(T) 和 sizeof(void*)?
因为当内存块被释放时,需要作为自由链表的节点存储「下一个节点的地址」(一个指针)。如果 T 类型的大小小于指针大小(例如 T 是 char),直接用 sizeof(T) 分配内存会导致存储指针时越界。因此,强制按「指针大小」分配,确保内存块有足够空间存储链表节点信息。

分割后,_memory 指针向后移动 objSize 字节,_remainBytes 减去相应大小,记录剩余可用内存。
在这里插入图片描述

//调用构造函数
new(obj)T;

普通 new 会做两件事:1. 分配内存;2. 调用构造函数。
这里用 placement new(定位 new),仅在已分配的内存(obj 指向的地址)上调用 T 的构造函数,完成对象初始化

这是内存池的关键:内存分配由池管理,构造函数仅负责对象的逻辑初始化。

可以理解上述所有操作都是在给这一步做准备

3 Delete()

// 释放对象
    void Delete(T* obj)
    {
        // 1. 调用对象析构函数
        obj->~T();

        // 2. 将内存块加入自由链表(头插法)
        *(void**)obj = _freeList; // 存储下一个自由节点地址
        _freeList = obj; // 更新自由链表头
    }

这段 Delete 函数是定长内存池释放对象的核心逻辑,负责将不再使用的对象内存回收至 “自由链表”(供下次复用),同时确保对象资源被正确清理。

obj->~T();

显式调用析构函数,如果上述的构造函数是最后准备的结果,那现在这个析构就是准备的第一步

// 头插:将当前内存块作为新的链表头
*(void**)obj = _freeList;  // 步骤1:当前块存储下一个空闲块的地址
_freeList = obj;           // 步骤2:更新链表头为当前块

上面讲解过了为什么要*(void**),而这里原理一样,是为了将删除的空内存作为_freeList的头节点
在这里插入图片描述

4 测试

#include "objectpool.hpp"

void TestObjectPool()
{
	// 申请释放的轮次
	const size_t Rounds = 5;

	// 每轮申请释放多少次
	const size_t N = 100000;

	std::vector<TreeNode*> v1;
	v1.reserve(N);

	size_t begin1 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v1.push_back(new TreeNode);
		}
		for (int i = 0; i < N; ++i)
		{
			delete v1[i];
		}
		v1.clear();
	}

	size_t end1 = clock();

	std::vector<TreeNode*> v2;
	v2.reserve(N);

	ObjectPool<TreeNode> TNPool;
	size_t begin2 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v2.push_back(TNPool.New());
		}
		for (int i = 0; i < N; ++i)
		{
			TNPool.Delete(v2[i]);
		}
		v2.clear();
	}
	size_t end2 = clock();

	cout << "new cost time:" << end1 - begin1 << endl;
	cout << "object pool cost time:" << end2 - begin2 << endl;
}

int main() {
	TestObjectPool();
	return 0;
}

在这里插入图片描述
如果把释放次数加0变成1000000,两者差别更明显:
在这里插入图片描述

五、定长内存池与普通new的区别

对比维度 普通 new/delete 定长内存池
内存来源 每次分配直接向系统申请,释放直接归还给系统 预先向系统申请大块内存,后续分配从该块中分割
内存复用机制 无主动复用逻辑,依赖系统内存管理器的优化 通过自由链表记录释放的内存块,下次分配优先复用
支持的对象大小 任意大小(按需分配) 仅支持固定大小(针对特定类型 T)
分配 / 释放效率 低。每次操作可能涉及系统调用(用户态→内核态切换)和复杂的内存块搜索 高。分配 / 释放均为 O (1) 的链表操作,仅在内存不足时触发一次系统调用
内存碎片 严重。频繁分配 / 释放不同大小内存会产生大量细碎空闲块 几乎无碎片。所有内存块大小统一,释放后可被同类型对象复用
适用场景 低频次、大小不固定的内存分配(如偶尔创建大对象) 高频次、同类型小对象的分配 / 释放(如链表节点、树节点)
额外内存开销 有(系统需维护内存块元信息,如大小、边界标记) 几乎无(利用对象自身内存存储自由链表指针)

网站公告

今日签到

点亮在社区的每一天
去签到