【内存管理与高并发内存池系列】从 malloc 到 ObjectPool:定长内存池的原理、对齐处理与空闲链表复用 本文专栏内存管理与高并发内存池作者主页努力努力再努力wz今日博客励志语录你现在走得慢没关系怕的是你明明还有火却因为一时看不到结果就把自己熄灭了。★★★ 本文前置知识glibc内存分配机制思维导图引入在此前的学习中我们已经认识了mmap系统调用以及malloc函数背后的内存分配与管理机制。当程序需要在堆上申请内存时通常会调用malloc。malloc本身是线程安全的多个线程可以并发调用它申请和释放内存。但是线程安全并不意味着没有额外成本。结合前面对malloc执行路径的分析可以知道如果线程申请的内存能够命中当前线程的tcache那么分配过程可以直接在线程局部缓存中完成效率相对较高。但是一旦tcache没有命中例如对应大小的空闲链表为空或者申请的内存大小不在tcache的管理范围内那么malloc就可能需要进入对应arena的分配路径。而arena是可能被多个线程共享的结构因此访问arena时就可能涉及加锁。对于频繁调用malloc申请内存、又频繁调用free释放内存的程序来说这部分锁开销和分配路径上的管理开销就可能被不断放大。因此内存池要解决的核心问题并不是完全替代malloc而是针对特定场景减少频繁调用malloc/free带来的开销。以定长内存池为例它会预先向系统申请一片连续的内存空间然后将这片内存按照固定大小切分成一个个内存块。后续程序需要申请内存时不再频繁调用malloc而是直接从已经切分好的空闲内存块中取出一个当内存释放时也不会立即归还给系统而是重新挂回内存池的空闲链表中等待下一次复用。这就是定长内存池的核心思想提前申请一批内存并通过空闲链表管理已经申请好的内存块从而将后续频繁的申请和释放操作转化为对空闲链表的取出与归还减少对malloc/free的依赖。定长内存池原理图解从 malloc 对比到空闲链表复用定长内存池的设计思想固定大小内存块与空闲链表复用根据上文的分析我们已经知道定长内存池的核心思想是先预先向操作系统申请一片连续的内存空间然后由程序自己管理这片内存。这样一来后续在高频申请和释放内存时就不需要反复调用malloc和free从而减少通用内存分配路径中的锁竞争以及管理开销。不过在具体设计定长内存池之前我们还需要先明确一点定长内存池虽然可以借鉴malloc中“切块管理”和“空闲链表复用”的思想但是它和malloc的设计目标并不相同。malloc是一个通用内存分配器它需要处理各种不同大小的内存申请。也就是说程序可能申请 16 字节也可能申请 64 字节、128 字节甚至更大的内存块。因此malloc内部需要按照不同规格来组织和管理内存块并且每个 chunk 还需要维护对应的元数据例如当前 chunk 的大小、状态信息等。之所以必须这样,是因为free只接收一个指针,分配器要能反查出这块的大小、以及前后块是否空闲可合并,这些信息没有别处可放,只能记录在 chunk 头部。而定长内存池面向的场景更加具体。它通常并不是为了处理任意大小的内存申请而是针对某一种固定大小对象的高频创建和销毁。例如在我之前实现的 Epoll Server 项目中主线程会不断接收客户端连接。每当一个 TCP 连接完成三次握手之后程序就需要创建一个连接对象用来保存该连接对应的上下文信息当连接关闭之后又需要释放对应的连接对象。在这种高并发场景下连接对象的申请和释放会非常频繁。但是这些对象的类型是固定的大小也是固定的。也就是说在连接建立和关闭这类高频执行的逻辑中程序并不是反复申请各种不同大小的对象而是反复申请和释放同一种连接对象。因此对于定长内存池来说我们就不需要像malloc那样设计成一个通用的内存分配器也不需要维护多种不同大小规格的内存块。我们只需要将预先申请到的连续内存切分成多个大小相同的内存块即可。每一个内存块都可以用来存放一个固定类型的对象。也正是因为每个内存块的大小都是固定的所以定长内存池不需要像malloc chunk那样在每个内存块前面额外维护 chunk 大小等元数据信息。内存池只需要在整体结构中记录单个内存块的大小、当前剩余空间的位置、空闲链表的头指针等管理信息即可。对于每一个已经分配出去的内存块来说它本身就可以完全作为对象的存储空间使用。接下来还需要考虑释放之后的内存块如何复用。由于对象的释放顺序和申请顺序并不一定一致所以内存池中被释放的空闲块可能分布在不同位置并不是连续排列的。针对这些不连续的空闲块我们可以使用一个空闲链表将它们组织起来。这里的空闲链表不需要额外创建链表节点而是可以直接复用空闲块本身。因为一个内存块被释放之后它原本保存的对象数据已经失效这部分空间就可以被重新利用。于是我们可以把空闲块头部的几个字节当作next指针用来保存下一个空闲块的地址从而将多个空闲块串联成一条链表。这样一来当程序再次申请内存块时内存池会优先检查空闲链表。如果空闲链表不为空就直接从链表头部取出一个空闲块进行复用如果空闲链表为空再从尚未切分的连续内存区域中划分出一个新的内存块。所以定长内存池的核心可以概括为针对固定大小对象提前申请一片连续内存将其切分成多个相同大小的内存块并通过空闲链表管理已经释放的内存块从而实现内存块的快速复用。示意图定长对象池的代码实现从内存布局到对象申请与释放ObjectPool 代码骨架设计内存块大小计算与对齐处理根据上文的分析我们已经认识了定长内存池的基本原理。接下来就可以开始编写对应的代码。在正式实现接口之前我们先明确整体代码骨架。对于定长内存池来说它的核心职责就是提前申请一片连续内存并将这片内存按照固定大小切分成多个内存块后续再通过空闲链表对这些内存块进行管理和复用。因此这里我们可以设计一个ObjectPool模板类。之所以将其设计成模板类是因为对象池本身应该具备一定的通用性。不同场景下我们可能需要为不同类型的对象创建定长内存池例如连接对象、任务对象、定时器节点等。通过模板参数T就可以指定当前对象池负责管理哪一种类型的对象。接下来需要在ObjectPool类中维护定长内存池的管理信息。首先需要有一个指针指向空闲链表的头节点用来管理已经释放、可以再次复用的内存块。其次还需要记录整片内存池的起始位置、当前尚未划分区域的起始位置以及整片内存池的结束位置。这样一来当空闲链表中没有可复用内存块时我们就可以继续从尚未划分的连续内存区域中切出新的内存块。除此之外还需要记录当前对象池最多能够容纳多少个内存块。这里需要注意构造函数接收的poolSize表示的是内存池中预先准备的内存块数量而不是字节数。真正申请的总字节数应该是poolSize*slotSize其中slotSize表示每一个内存块的实际大小。对于构造函数来说它会接收一个参数用来表示当前对象池预先创建多少个固定大小的内存块。获取到这个参数之后首先需要判断其合法性。如果传入的块数量为 0那么这个对象池就没有实际意义因此直接抛出异常。其次为了避免一次性申请过大的内存空间这里也可以设置一个上限如果超过该上限同样抛出异常。完成参数检查之后就需要向系统申请一片连续的虚拟内存。这里既可以调用mmap申请也可以调用malloc申请。为了让实现更加简单这里选择使用malloc。malloc返回的是这片连续内存的起始地址拿到这个地址之后就可以初始化start、cur和end等指针。不过在真正申请内存之前还有一个非常关键的细节需要处理那就是每一个内存块的大小也就是slotSize。这里的块大小不能简单地等于sizeof(T)。原因在于一个内存块存在两种状态当它被分配出去时它用来存放T类型对象当它被释放之后它会被复用为空闲链表中的一个节点。此时空闲块头部的几个字节会被用来保存下一个空闲块的地址。也就是说一个内存块既要能够容纳一个T类型对象也要能够容纳一个FreeNode节点。例如在 64 位平台下指针大小通常是 8 字节。如果某个对象本身只有 4 字节那么如果直接将块大小设置为sizeof(T)释放该对象之后再把这个 4 字节空间复用成一个保存指针的空闲链表节点就会发生越界访问。因此这里需要先取sizeof(T)和sizeof(FreeNode)中的较大值size_t rawSizestd::max(sizeof(T),sizeof(FreeNode));这里除了要保证每个内存块的空间大小足够之外还需要额外考虑内存对齐问题。在 C 中一个对象的起始地址需要满足该类型的对齐要求。也就是说如果某个类型的对齐数是alignof(T)那么该类型对象的起始地址就应该是alignof(T)的整数倍。这里需要注意对齐要求并不等同于对象大小。对象大小由sizeof(T)表示而对象的对齐要求由alignof(T)表示。这里补充说明一下alignof运算符。alignof是 C 中用来获取类型对齐要求的运算符其语法形式为alignof(T)表示T类型对象在内存中存放时起始地址需要满足多少字节对齐。需要注意的是alignof(T)和sizeof(T)表示的是两个不同概念。sizeof(T)表示一个T类型对象本身占用多少字节而alignof(T)表示这个对象的起始地址需要按照多少字节进行对齐。例如对于一个int类型来说它的大小通常是 4 字节对齐要求通常也是 4 字节。这意味着一个int对象最好放在 4 的整数倍地址上。如果我们只是从字节数组的角度看某个地址后面确实有足够的空间可以存放一个int但是这个地址本身不满足int的对齐要求那么在这个地址上构造或访问int对象就是不安全的。从硬件角度来看CPU 访问内存时通常更倾向于按照特定边界读取数据。比如对于一个 4 字节的数据来说如果它的起始地址刚好是 4 的整数倍那么这个数据就完整地落在一个符合对齐要求的地址范围内。CPU 在访问这个数据时通常可以更加自然地一次性取出完整内容。但是如果一个 4 字节数据的起始地址不是 4 的整数倍例如从某个偏移 1 字节的位置开始存放那么这个数据就可能跨越原本的访问边界。此时CPU 可能需要进行多次内存读取再将读取到的结果重新拼接起来才能得到完整的数据。这样不仅会增加额外的访问开销也会降低整体访问效率。因此满足对齐要求的数据通常能够被 CPU 更高效地访问而不满足对齐要求的数据轻则导致额外的读取与拼接开销重则在某些对齐要求严格的硬件平台上可能直接触发硬件异常。从 C 语言规则来看如果在一块不满足T对齐要求的地址上构造T对象那么这个行为本身就是未定义行为。因此内存池在切分内存块时不能只考虑每个块是否能够容纳一个对象还必须保证每个块的起始地址满足对象的对齐要求。所以在计算每个内存块大小时首先需要取sizeof(T)和sizeof(FreeNode)中的较大值保证该内存块既能存放对象也能在空闲状态下复用成空闲链表节点。接着还需要取alignof(T)和alignof(FreeNode)中更严格的对齐要求并将块大小向上调整到该对齐数的整数倍。这样一来只要整片内存池的起始地址满足对齐要求并且每个内存块的大小也是对齐数的整数倍那么后续通过start n * slotSize切分出来的每一个内存块其起始地址也都能够满足对齐要求。所以这里还需要取alignof(T)和alignof(FreeNode)中更严格的对齐要求size_t alignmentstd::max(alignof(T),alignof(FreeNode));最后再将rawSize按照该对齐数进行向上对齐得到最终的slotSizeslotSizealign_up(rawSize,alignment);这里之所以需要将rawSize按照alignment向上对齐是因为rawSize只能保证单个内存块的空间大小足够却不能保证后续每一个内存块的起始地址都满足对齐要求。在定长内存池中整片连续内存会被按照固定大小切分成多个内存块。假设整片内存的起始地址为start每个内存块的大小为slotSize那么每个内存块的起始地址大致如下start startslotSize start2*slotSize start3*slotSize也就是说后续每一个内存块的起始地址都是在start的基础上不断加上slotSize得到的。因此即使start本身满足对齐要求如果slotSize不是alignment的整数倍那么后续切分出来的内存块起始地址就可能逐渐偏离对齐边界。举个例子假设某个类型对象本身占 12 字节而空闲链表节点FreeNode的对齐要求是 8 字节那么此时rawSize可能是 12alignment可能是 8。如果直接让slotSize rawSize也就是每个块大小为 12 字节那么即使第一个块的起始地址满足 8 字节对齐第二个块的起始地址也会变成start 12这个地址就不一定再是 8 的整数倍。这样一来后续某些内存块在被释放之后如果要复用成FreeNode节点就可能出现地址不满足FreeNode对齐要求的问题。同理当这些块再次被用来构造T类型对象时也可能不满足T类型的对齐要求。因此slotSize不能直接等于rawSize而是需要将rawSize向上调整到alignment的整数倍slotSizealign_up(rawSize,alignment);这样做的目的就是保证每个内存块的大小本身是对齐数的整数倍。只要整片内存的起始地址满足对齐要求那么后续通过start n * slotSize切分出来的每一个内存块其起始地址也都能够继续满足对齐要求。所以rawSize解决的是“当前内存块是否放得下”的问题而align_up解决的是“连续切分之后每一个内存块起始地址是否仍然对齐”的问题。两者结合起来才能保证对象池中的每一个内存块既有足够空间又能安全地存放T对象或复用成FreeNode节点。为此我们可以自己实现一个向上对齐函数align_up专门用于把某个大小调整到指定对齐数的整数倍staticsize_talign_up(size_t size,size_t alignment){return(sizealignment-1)/alignment*alignment;}这个函数的含义是如果size本身已经是alignment的整数倍那么结果仍然是size如果不是就将其向上调整到下一个alignment的整数倍。例如align_up(12, 8)的结果就是 16。对应的代码骨架如下templatetypenameTclassObjectPool{private:structFreeNode{FreeNode*next;};public:ObjectPool(size_t _poolSize):slotSize(0),start(nullptr),cur(nullptr),end(nullptr),poolsize(_poolSize),freeListHead(nullptr){if(poolsize0){throwstd::invalid_argument(Pool size must be greater than 0);}if(poolsize1000000){throwstd::invalid_argument(Pool size is too large);}size_t rawSizestd::max(sizeof(T),sizeof(FreeNode));size_t alignmentstd::max(alignof(T),alignof(FreeNode));slotSizealign_up(rawSize,alignment);startstatic_castchar*(std::malloc(poolsize*slotSize));if(startnullptr){throwstd::bad_alloc();}curstart;endstartpoolsize*slotSize;}// ...private:staticsize_talign_up(size_t size,size_t alignment){return(sizealignment-1)/alignment*alignment;}size_t slotSize;char*start;char*cur;char*end;size_t poolsize;FreeNode*freeListHead;};在这段代码中start指向整片内存池的起始位置end指向整片内存池的结束位置cur指向当前尚未被划分区域的起始位置。后续当空闲链表为空时就可以通过cur从剩余空间中继续切分出新的内存块。而freeListHead则指向空闲链表的头节点。当有内存块被释放时该内存块会被挂到空闲链表中当再次申请内存块时优先从空闲链表中取出一个已经释放的内存块进行复用。因此这个类的骨架实际上已经包含了定长内存池最核心的几个组成部分固定大小的内存块、连续内存区域、当前切分位置以及空闲链表。New 接口实现空闲块复用与 placement new 构造对象有了ObjectPool类的基本骨架之后接下来就可以继续实现对象池对外提供的接口。对于一个简单的定长对象池来说核心接口其实并不复杂主要就是两个一个用于申请对象的New接口另一个用于释放对象的Delete接口。这里我们先来看New接口的实现。New接口的职责并不是简单地返回一块原始内存而是从对象池中取出一块固定大小的内存并在这块内存上构造一个T类型对象最终返回构造完成后的对象指针。它的整体分配思路可以分为两步。首先优先检查空闲链表是否为空。如果空闲链表不为空说明此前已经有对象被释放对应的内存块已经被挂回到了空闲链表中。此时就可以直接取出空闲链表的头节点进行复用并让freeListHead指向下一个空闲块。如果空闲链表为空说明当前没有已经释放的内存块可以复用。此时就需要从尚未切分的连续内存区域中继续划分出一个新的内存块。划分之前需要先判断剩余空间是否足够。如果cur slotSize end说明当前对象池中既没有可以复用的空闲块也没有足够的未划分空间此时对象池已经无法继续分配只能返回空指针。如果剩余空间足够就将当前cur指向的位置作为本次分配的内存块起始地址然后让cur向后移动slotSize指向下一块尚未划分区域的起始位置。拿到可用内存之后还需要调用 placement new 在这块已有内存上构造T类型对象returnnew(memory)T();这里需要注意placement new 并不会重新向系统申请内存它的作用是在指定的内存地址上调用对象的构造函数。也就是说memory指向的内存块已经由对象池提前准备好了placement new 只是负责在这块内存上完成对象的构造。对应代码如下T*New(){void*memorynullptr;if(freeListHead!nullptr){memoryfreeListHead;freeListHeadfreeListHead-next;}else{if(curslotSizeend){returnnullptr;}memorycur;curslotSize;}returnnew(memory)T();}当前这个版本的New接口调用的是T的默认构造函数因此它要求T类型必须支持默认构造。如果某个对象需要通过带参构造函数完成初始化那么可以进一步将New接口改造成可变模板参数版本从而支持在对象池中直接构造带参数的对象。不过对于当前这个入门版本来说先实现默认构造版本即可。它已经能够清楚体现定长对象池的核心分配流程优先复用空闲链表中的内存块如果没有可复用块再从尚未切分的连续内存区域中划分新块最后通过 placement new 在该内存块上构造对象。Delete 接口实现显式析构与空闲链表回收最后我们再来看Delete接口的实现。Delete接口的作用并不是将内存真正归还给操作系统而是将已经使用完的对象归还给对象池等待后续再次复用。因为在New接口中我们是通过 placement new 在指定内存块上构造了一个T类型对象所以在归还这块内存之前首先需要显式调用对象的析构函数结束该对象的生命周期。对应代码如下voidDelete(T*obj){if(objnullptr){return;}obj-~T();FreeNode*nodereinterpret_castFreeNode*(obj);node-nextfreeListHead;freeListHeadnode;}首先函数会判断传入的对象指针是否为空。如果obj nullptr说明当前没有有效对象需要释放直接返回即可。如果对象指针有效那么第一步就是调用对象的析构函数obj-~T();这里需要注意调用析构函数只是销毁obj指向的这个T类型对象释放对象内部可能持有的资源例如堆内存、文件描述符、缓冲区等。但是obj指向的这块原始内存本身并没有被释放它仍然属于当前对象池管理。当析构函数调用完成之后这块内存中原本保存的对象数据就已经失效。此时这块内存就可以重新被复用。由于空闲链表的节点结构非常简单只需要保存下一个空闲块的地址所以我们可以直接将这块内存重新解释成一个FreeNode节点FreeNode*nodereinterpret_castFreeNode*(obj);这里再补充一个细节当我们说“复用空闲块头部的几个字节来保存下一个空闲块的地址”时在代码层面通常有两种写法。第一种写法就是定义一个简单的结构体节点例如structFreeNode{FreeNode*next;};当某个对象被释放之后这块内存中原本保存的对象数据已经失效此时就可以将这块内存的起始地址强制转换为FreeNode*FreeNode*nodereinterpret_castFreeNode*(obj);node-nextfreeListHead;freeListHeadnode;这样做的含义是把这块空闲内存的起始位置重新解释成一个FreeNode节点。由于FreeNode内部只有一个next指针所以编译器会按照结构体成员的布局规则将这块内存开头的若干字节解释为next成员。也就是说空闲块头部的几个字节就被用来保存下一个空闲块的地址。第二种写法则更加底层可以直接使用void**来操作空闲块头部的指针空间。例如*(void**)objfreeListHead;freeListHeadreinterpret_castFreeNode*(obj);这里的核心是我们希望在obj指向的这块内存开头写入一个地址而地址本身就是一个指针值。因此可以先把obj转换成void**也就是“指向指针的指针”即二级指针然后再通过解引用操作*(void**)obj把空闲块头部的sizeof(void*)个字节当作一个指针变量来使用。换句话说(void**)obj表示将obj这个地址强制转换为void**类型也就是把它看作一个二级指针。其含义是把obj指向的这块内存的起始位置当成一个“可以存放指针值的位置”。而*(void**)objfreeListHead;表示向这块内存的头部写入下一个空闲块的地址。这两种写法的本质是一样的都是复用空闲块自身的前几个字节来保存next指针从而把多个空闲块串联成一条空闲链表。相比之下使用FreeNode结构体的写法语义更加清晰也更适合放在博客和代码示例中而void**写法更加底层能更直观地体现“直接把空闲块头部当作指针槽位使用”的思想。无论采用哪一种写法都必须保证每个内存块至少能够容纳一个指针并且内存块的起始地址满足指针类型的对齐要求。否则当空闲块被复用成链表节点时就可能出现越界访问或未对齐访问的问题。接着通过头插法将该节点挂入空闲链表中node-nextfreeListHead;freeListHeadnode;这样一来这个已经释放的内存块就重新回到了对象池的空闲链表中。后续再次调用New接口申请对象时就可以优先从空闲链表中取出该内存块进行复用而不需要重新向系统申请内存。因此Delete接口的核心流程可以概括为先显式调用析构函数销毁对象再将对象占用的内存块复用成空闲链表节点并通过头插法归还给对象池。需要注意的是当前实现默认传入的obj一定来自当前对象池。如果传入一个不是由该对象池分配的指针或者对同一个对象重复调用Delete都可能破坏空闲链表结构导致未定义行为。源码ObjectPool.hpp:#pragmaonce#includecstdlib#includestdexcept#includenew#includealgorithmtemplatetypenameTclassObjectPool{private:structFreeNode{FreeNode*next;};public:/** * 对象池的构造函数用于初始化对象池 * _poolSize 指定对象池的大小对象数量 */ObjectPool(size_t _poolSize):slotSize(0)// 初始化每个槽位的大小为0,start(nullptr)// 初始化内存起始指针为nullptr,cur(nullptr)// 初始化当前指针为nullptr,end(nullptr)// 初始化结束指针为nullptr,poolsize(_poolSize)// 初始化对象池大小,freeListHead(nullptr)// 初始化空闲链表头为nullptr{// 检查对象池大小是否合法if(poolsize0){throwstd::invalid_argument(Pool size must be greater than 0);}// 检查对象池大小是否过大if(poolsize1000000){throwstd::invalid_argument(Pool size is too large);}// 计算每个槽位的大小和对齐要求size_t rawSizestd::max(sizeof(T),sizeof(FreeNode));// 获取T类型和FreeNode类型中较大的大小size_t alignmentstd::max(alignof(T),alignof(FreeNode));// 获取T类型和FreeNode类型中较大的对齐要求slotSizealign_up(rawSize,alignment);// 对齐后的槽位大小// 分配内存startstatic_castchar*(std::malloc(poolsize*slotSize));// 分配连续内存块if(startnullptr){throwstd::bad_alloc();// 内存分配失败时抛出异常}// 初始化指针curstart;// 当前指针指向内存起始位置endstartpoolsize*slotSize;// 结束指针指向内存末尾}// 析构函数用于释放对象池分配的内存~ObjectPool(){// 释放通过start指针分配的内存块std::free(start);}ObjectPool(constObjectPool)delete;ObjectPooloperator(constObjectPool)delete;/** * 分配内存并构造新对象的函数 * * return T* 返回指向新构造对象的指针如果内存不足则返回nullptr */T*New(){void*memorynullptr;// 用于存储分配的内存地址// 检查空闲列表是否有可用节点if(freeListHead!nullptr){// 从空闲列表中获取内存块memoryfreeListHead;freeListHeadfreeListHead-next;// 更新空闲列表头指针}else{// 检查当前内存池是否有足够空间if(curslotSizeend){returnnullptr;// 内存不足返回nullptr}// 从内存池中分配新内存块memorycur;curslotSize;// 更新当前指针}// 在分配的内存上构造新对象returnnew(memory)T();}/** * 回收对象并将其内存返回给内存池的空闲链表 * obj 指向待删除对象的指针 * * 注意此函数仅调用析构函数清理对象资源并将内存交还给内存池 * 并未真正将内存归还给操作系统即没有调用系统的 free/delete。 */voidDelete(T*obj){// 安全检查如果传入的指针为空直接返回避免对空指针解引用引发崩溃if(objnullptr){return;}// 1. 显式调用对象的析构函数// 这样做可以正确清理对象内部管理的资源如动态内存、文件句柄等// 但不会释放对象自身占用的内存块即不调用 operator delete。obj-~T();// 2. 将对象占用的内存块重新纳入内存池的空闲链表// 使用 reinterpret_cast 将对象指针强转为 FreeNode*。// 这里利用了“复用内存块”的技巧对象被析构后其占用的内存空间// 在逻辑上已经变成了一块裸内存直接将这块内存的前几个字节// 当作 FreeNode 的 next 指针来使用从而避免了额外的内存分配开销。FreeNode*nodereinterpret_castFreeNode*(obj);// 3. 头插法将当前空闲节点插入到空闲链表的头部// 将新回收的节点的 next 指向原来的链表头node-nextfreeListHead;// 更新链表头指针使其指向刚刚回收的节点freeListHeadnode;}private:staticsize_talign_up(size_t size,size_t alignment){return(sizealignment-1)/alignment*alignment;}private:size_t slotSize;char*start;char*cur;char*end;size_t poolsize;FreeNode*freeListHead;};结语那么这就是本篇文章的全部内容我会持续更新希望你能够多多关注如果本文有帮助到你的话还请三连加关注你的支持就是我创作的最大动力