找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5563

积分

0

好友

712

主题
发表于 4 天前 | 查看: 3| 回复: 0

在 C++ 程序开发中,内存管理始终是一道绕不开的核心难题。传统的动态内存分配方式有着明显的痛点:频繁调用 new/delete 会不断切割堆空间,产生大量内存碎片,零散的空闲内存块难以被有效复用,内存利用率随之下降。与此同时,每一次系统调用的触发都在累积额外开销,拖慢程序运行效率。在高并发、高频内存操作的场景下,这些问题更容易演变成程序卡顿甚至异常。

内存池技术恰好能破解这些困境。它的核心思路是“预分配 + 复用”:先向系统一次性申请一块连续内存,然后按需求划分成固定或可变大小的内存块,后续的分配和回收都在池内完成。这样既从源头减少了碎片,也大幅降低了系统调用频率。掌握 C++ 内存池的实现逻辑,既能针对性地优化程序性能,也能加深对 C++ 底层内存机制的理解。

一、C++ 内存池简介

内存池是一种内存分配方式,也被称为固定大小区块规划(fixed-size-blocks allocation)。我们平时习惯直接使用 newmalloc 等 API 申请内存,但申请的内存块大小不固定,频繁使用就很容易产生大量内存碎片,拖累整体性能。

在 Linux 内核中,有不少地方的内存分配不允许失败。作为一种确保极端情况下仍能完成分配的手段,内核开发者创建了内存池(mempool)这一抽象。内存池本质上是一类后备缓存,它会尽力维持一份空闲内存列表,留给紧急情况使用。

1.1 为什么要用内存池?

C++ 程序默认的内存管理(newdeletemallocfree)会频繁地在堆上分配和释放内存,带来性能损失和内存碎片,同时降低内存利用率。默认的内存管理器为了通用性,无法在性能上做到极致。因此,很多场景需要根据业务特点设计专用内存管理器,内存池就是其中的典型代表。

(1) 内存碎片问题

造成堆利用率低的一个重要原因就是内存碎片化。当系统存在未使用的存储空间,但这块空间无法满足具体的分配请求时,就产生了内存碎片化问题。内存碎片化分为内部碎片和外部碎片两类。

  • 内碎片:当已分配的块比有效载荷大时产生。比如分配了 10 个字节的空间,实际只用了 5 个字节,剩下 5 字节就是内碎片。内部碎片的大小等于已分配块大小与有效载荷之差的累加,因此它既取决于内存请求模式,也取决于分配器的对齐实现。
  • 外碎片:假设系统依次分配了 16byte、8byte、16byte、4byte,还剩 8byte 未分配。此时若要分配一个 24byte 的连续空间,即便回收了前面两个 16byte,总剩余有 40byte,但依然拼不出连续的 24byte 空间,这就是外碎片问题。

C++内存布局示意图:栈、共享映射区、堆、数据段、代码段与内存块分配

(2) 申请效率问题

打个比方,一学期的生活费是 6000 块:

  • 方式一:开学时 6000 块一次性给你,自己保管和分配。
  • 方式二:每次花钱时都联系父母转账。

同样是 6000 块钱,第一种方式显然效率更高,因为第二种方式与父母的沟通交互成本太高了。

程序的处境也是如此。操作系统就像父母,当程序频繁申请内存时,每次都向系统要,效率必然受影响。

1.2 内存池原理

内存池的核心思想是:在真正使用内存之前,先预申请一定数量、大小预设的内存块备用。当有新需求时,就从池子中分出一部分内存块;如果不够,再继续申请新的。内存释放后回归池中,留待后续复用。这样一来,内存使用效率得到提升,也不会产生不可控的内存碎片。

内存池设计算法原理:

  • 预申请一个内存区 chunk,将内存按对象大小划分成多个内存块 block。
  • 维护一个空闲内存块链表,通过指针相连,头指针指向第一个空闲块。
  • 每次申请对象空间时,将该内存块从空闲链表中取出,更新链表头指针。
  • 每次释放对象空间时,将该内存块重新插回空闲链表头部。
  • 如果一个内存区占满,则新开辟一个内存区,维护内存区链表,头指针指向最新的内存区,新块从该区重新划分。

通用内存分配和释放的缺点:

  • 使用 malloc/new 申请堆内存时,系统需要按照最先匹配、最优匹配等算法在空闲块表中查找;使用 free/delete 释放时,系统可能需要合并空闲内存块,产生额外开销。
  • 频繁使用时会产生大量内存碎片,降低程序运行效率。
  • 容易造成内存泄漏。

内存池(Memory Pool)是替代直接调用 malloc/freenew/delete 的常用方案。申请内存空间时,直接从池中查找合适的内存块,而不是每次都向操作系统伸手。

内存池技术的优点:

  • 堆内存碎片很少。
  • 内存申请/释放比 malloc/new 方式更快。
  • 可以检查任何一个指针是否在内存池中。
  • 支持将堆转储(Heap-Dump)写入硬盘。
  • 具备内存泄漏检测能力:当存在未释放的内存时,内存池会抛出断言(assertion)。

内存池可以分为不定长内存池和定长内存池两类。不定长内存池的典型实现有 Apache Portable Runtime 中的 apr_pool 和 GNU libc 中的 obstack;定长内存池则有 boost_pool 等。不定长内存池不需要为不同数据类型创建不同的池子,但缺点是无法将分配出的内存回收到池内。定长内存池则支持在使用完毕后归还内存,但需要为不同类型的数据结构分别创建内存池。

二、C++ 内存碎片与低效

在 C++ 的世界里,内存管理是一项至关重要却又充满挑战的任务。我们编写程序时,常常会用到 newdelete(或者 mallocfree)来进行动态内存的分配与释放。比如创建对象或数组时用 new 获取内存,使用完毕后再用 delete 释放。

int* ptr = new int;
*ptr = 10;
// 使用ptr
delete ptr;

然而,这种看似直接的内存管理方式,在频繁分配与释放的场景下会暴露出两个突出问题——内存碎片和效率低下。

2.1 内存碎片问题

程序运行过程中,如果不断进行小块内存的分配与释放,内存空间就会变得越来越零散。就好比一个书架上,起初所有书都整齐摆放,空间充足。但随着不断借书(分配)和还书(释放),书架上会出现许多零散的小空位。这些小空位无法被充分利用,即使后续有新书要放,也可能因为没有足够大的连续空位而无法放置,内存利用率因此降低。

内存碎片分为内部碎片和外部碎片。内部碎片是指分配的内存块大于实际所需,多余部分无法利用;外部碎片则是频繁分配和释放导致空闲内存分散成许多小块,无法满足较大内存块的分配需求。

2.2 效率低下问题

每次使用 newdelete 操作内存,都需要与操作系统交互,涉及系统调用等开销,会消耗一定的时间。在游戏开发中大量创建和销毁对象,或者网络编程中频繁处理小数据包时,这种开销会不断累积,严重影响运行效率。想象一下,每次取工具都要跑到很远的仓库去拿,用完再送回去,如此频繁操作,效率必然大打折扣。同理,C++ 程序频繁地向操作系统请求和归还内存,也会让执行效率大打折扣。

要解决这些问题,内存池技术应运而生。它就像一个高效的内存管家,能够更合理地管理内存,提升程序的性能和稳定性。

三、C++ 内存池技术详解

3.1 为什么要使用内存池

  • 解决内碎片问题。
  • 由于向内存申请的内存块比较大,能够降低外碎片问题。
  • 一次性申请一块大内存慢慢使用,避免频繁向系统请求内存,提高内存分配效率。
  • 但内碎片问题无法完全避免,只能尽可能降低。

3.2 内存池的演变

最简单的内存分配器,是做一个链表指向空闲内存。分配时取出一块,改写链表后返回;释放时放回链表并做好归并。实现中需要注意做好标记和保护,避免二次释放。还可以花些精力优化查找最适合大小的内存块,减少内存碎片,甚至把链表换成伙伴算法。

  • 优点:实现简单。
  • 缺点:分配时搜索合适内存块效率低,释放回归后归并消耗大,实际中不太实用。

进一步的方案是定长内存分配器,也就是实现一个 FreeList。每个 FreeList 负责分配固定大小的内存块,比如专门分配 32 字节对象的固定内存分配器。每个固定内存分配器内部有两个链表:OpenList 存储未分配的空闲对象,CloseList 存储已分配的对象。分配就是从 OpenList 中取出一个对象放入 CloseList 并返回给用户;释放则从 CloseList 移回 OpenList。如果 OpenList 不够用,就需要增长它:申请一块较大的内存,切割成比如 64 个相同大小的对象加入 OpenList。回收时,统一把先前向系统申请的内存块全部还给系统。

  • 优点:简单粗暴,分配和释放效率高,能有效解决特定场景下的问题。
  • 缺点:功能单一,只能满足定长内存需求,而且占着内存没有释放。

内存池内存管理示意图:memory区域与free_list链表结构

再进一步,是哈希映射的 FreeList 池。在定长分配器的基础上,按照不同对象大小(8、16、32、64、128、256、512、1k……64K)构造十多个固定内存分配器。分配内存时根据申请大小进行对齐,然后查哈希表决定由哪个分配器负责。分配后要在内存头部写入 cookie,标记该块内存由哪个分配器分配,这样释放时才能正确归还。如果大于 64K,则直接使用系统的 malloc。这种方案的分配时间近似 O(1),但缺点是某个 FreeList 在高峰期占用了大量内存后,即使之后闲置,也无法支援内存不足的其他 FreeList,无法达到分配均衡。

  • 优点:本质是定长内存池的改进,分配和释放效率高,能解决一定长度范围内的需求。
  • 缺点:存在内碎片问题;将大块内存切小后,申请大内存无法使用;多线程并发场景下锁竞争激烈,效率降低。

范例:SGI STL 六大组件中的空间配置器就是这种设计的实现。

二级空间配置器与free_list数组结构示意图:内存池与空闲链表管理

了解 malloc 底层原理

C 标准库函数 malloc 底层使用的是分离适配(segregated fit)。分配器维护一个空闲链表数组,每个空闲链表被组织成某种显式或隐式链表。每个链表包含大小不同的块,这些块的大小是大小类的成员。当需要分配时,先确定大小类,然后对相应的空闲链表做首次适配;如果没找到,就搜索下一个更大的大小类,直到找到合适的块。如果所有空闲链表中都没有合适块,就向操作系统请求额外的堆空间,从这个新堆空间中分配一个块,并将剩余部分放入适当的大小类。释放时执行合并,并将结果放到对应的空闲链表中。

  • malloc 优点:使用自由链表数组,提高分配释放效率;减少内存碎片,可以合并空闲内存。
  • malloc 缺点:维护隐式/显式链表需要额外空间,空间利用率不高;多线程下有线程安全问题,加锁解决又大幅降低效率。

3.3 内存池的核心原理

内存池的工作原理基于预先分配和重复利用策略。程序启动阶段,内存池一次性向操作系统申请一块较大的连续内存空间,就像提前租下一整栋大楼。随后,内存池将这块大内存按一定规则划分为多个较小的内存块,这些小内存块就像大楼里的一个个房间。

为了有效管理这些内存块,内存池通常借助链表和哈希表等数据结构。以链表为例,所有空闲内存块通过指针相互连接,形成空闲链表。当程序请求内存时,内存池从链表中取出一个合适的块分配出去;当程序释放内存时,该块重新插入空闲链表,等待下一次分配。哈希表则可以更快地定位到合适大小的内存块,通过将内存块大小作为键值,在近乎常数时间内找到满足需求的块,特别适合频繁分配不同大小内存块的场景。

3.4 内存池的显著优势

  • 减少内存碎片:内存池通过预先分配大块内存并在内部进行小块管理,避免了频繁向操作系统申请和释放小块内存导致的碎片问题。池内的分配和释放都在预分配范围内进行,保持了内存空间的连续性和规整性。
  • 提升分配释放效率:内存池的分配和释放操作在用户态完成,无需频繁与操作系统内核交互,避免了系统调用开销。再加上采用高效的数据结构和算法管理内存块,分配和释放操作更加快速。
  • 降低内存泄漏风险:内存池可以设计成具有自动回收机制。当某些内存块被遗忘释放时,内存池能在适当时机将其回收,重新纳入可分配池中,从而降低内存泄漏的可能性。

四、C++ 内存池实现方案

4.1 设计问题

设计内存池实现方案时,需要重点考虑以下问题:

① 内存池是否可以自动增长?

如果内存池的最大空间是固定的(非自动增长),那么池中内存被请求完后,程序就无法继续从池中获取内存。因此需要根据程序的实际内存使用情况来决定是否支持自动增长。

② 内存池的总内存占用是否只增不减?

如果内存池是自动增长的,就涉及总内存占用是否只增不减的问题。设想程序从自动增长的内存池中申请了 1000 个大小为 100KB 的内存片,使用完毕后全部归还。假设之后最多只会用到 10 个 100KB 的内存片,那么剩下的 900 个就一直闲置,程序内存占用降不下来。对内存占用大小有要求的程序需要特别考虑这一点。

③ 内存池中内存片的大小是否固定?

如果每次请求的内存片大小不固定,池中可用内存片的大小就不一致。再次请求时,内存池需要在“匹配最佳大小的内存片”和“匹配操作时间”之间做出权衡。最佳匹配可以减少内存浪费,但可能导致匹配时间变长。

④ 内存池是否是线程安全的?

是否允许多个线程同时从同一个内存池中请求和归还内存片?线程安全可以由内存池来实现,也可以由使用者来保证。

⑤ 内存片分配出去之前和归还到内存池之后,内容是否需要被清除?

程序可能出现将内存片归还给内存池后仍然使用原地址指针进行读写的情况,这会导致不可预期的结果。将内容清零只能尽量暴露问题,并不能从根本上解决,而且还会消耗 CPU 时间。最终的安全性还是需要由内存池的使用者来保证。

⑥ 是否兼容 std::allocator?

STL 标准库中的大多数容器都支持用户提供自定义内存分配器,默认使用 std::allocator,例如 std::string

typedef basic_string<char, char_traits<char>, allocator<char> > string;

如果内存池兼容 std::allocator,就可以用它来替换默认分配器:

typedef basic_string<char, char_traits<char>, MemoryPoll<char> > mystring;

4.2 常见内存池实现方案

(1) 固定大小缓冲池

适用于频繁分配和释放固定大小对象的场景。

(2) dlmalloc

dlmalloc 是由 Doug Lea 从 1987 年开始编写的内存分配器,目前最新版本为 2.8.3,因高效率被广泛使用和研究。

(3) SGI STL 内存分配器

SGI STL allocator 是目前设计最优秀的 C++ 内存分配器之一,内部 free_list[16] 数组负责管理从 8 bytes 到 128 bytes 不同大小的内存块(chunk),每个内存块都由连续固定大小的 chunk 组成,并用指针链表连接。

(4) Loki 小对象分配器

Loki 分配器使用 vector 管理数组,可以指定 fixed size block 的大小。free blocks 分布在一个连续的大内存块中,free chunks 可以根据使用情况自动增减合适数目,避免内存分配过多或过少。

(5) Boost object_pool

Boost object_pool 可以根据用户具体应用类的大小来分配内存块,通过维护一个 free nodes 链表来管理。它可以自动增加 nodes 块,初始 32 个 nodes,每次以两倍数量向系统堆申请内存。object_pool 管理的内存块需要在其对象销毁时才返还给系统堆。

(6) ACE_Cached_Allocator 和 ACE_Free_List

ACE 框架中包含一个可以维护固定大小内存块的分配器,通过在 ACE_Cached_Allocator 中定义 Free_List 链表来管理一个连续的大内存块,内存块中包含多个固定大小的未使用内存区块(free chunk),同时使用 ACE_unbounded_Set 维护已使用的 chunks。

(7) TCMalloc

Google 开源项目 gperftools 提供了内存池实现方案。TCMalloc 替换了系统的 malloc,在底层做了更多优化,性能更好。

4.3 STL 内存分配器

分配器(allocator)是 C++ 标准库的一个组件,主要用来处理所有给定容器(vector、list、map 等)内存的分配和释放。C++ 标准库提供了默认的通用分配器 std::allocator,开发者也允许自定义分配器。

GNU STL 除了提供默认分配器,还提供了 __pool_alloc__mt_allocarray_allocatormalloc_allocator 几种内存分配器。

  • __pool_alloc:SGI 内存池分配器。
  • __mt_alloc:多线程内存池分配器。
  • array_allocator:全局内存分配,只分配不释放,交给系统来释放。
  • malloc_allocator:对 std::mallocstd::free 的封装。

五、C++ 内存池的具体实现

5.1 内存池的多样类型

在 C++ 编程中,内存池根据分配内存块的方式和特点,可以分为不同类型,每种类型都有其独特的适用场景。了解这些类型和场景,能帮助我们更精准地选择和使用内存池,优化程序性能。

(1) 固定内存池

固定内存池每次分配的内存单元大小固定。程序初始化时就预先设定好内存块大小。例如,创建一个管理链表节点等小型数据结构的固定内存池,每个内存块设为 32 字节。当程序需要创建链表节点时,直接从池中获取 32 字节内存块,无需再向操作系统申请。

固定内存池的优点很明显:由于内存块大小固定,分配和释放操作非常高效,就像在一个装满同样大小盒子的仓库里取放物品,无需挑选和测量。而且,因为块大小一致,分配和回收过程中不会产生不同大小的内存空洞,能有效减少内存碎片。在游戏开发中,大量游戏对象(子弹、怪物等)具有相同的大小和结构,使用固定内存池管理这些对象的内存分配,可以显著提升性能和稳定性。

(2) 可变内存池

可变内存池更具灵活性,能根据实际需求分配不同大小的内存块。程序需要 10 字节存储小型数据和 100 字节存储较大数据结构时,可变内存池都能满足。

可变内存池适用于内存需求变化较大的场景。比如网络编程中,数据包大小不确定,可能是几十字节的小包,也可能是数千字节的大包。此时可变内存池能很好地适应变化。不过它的实现相对复杂,因为需要管理不同大小的内存块,分配和释放时需要更复杂的算法来确保高效利用并避免碎片,就像在一个装满各种大小物品的仓库里找东西,需要更多时间和精力去整理。

5.2 案例实现分析

(1) 案例分析

计划实现一个内存池管理类 MemoryPool,具备以下特性:

  • 内存池的总大小自动增长。
  • 内存池中内存片的大小固定。
  • 支持线程安全。
  • 内存片被归还后,清除其中的内容。
  • 兼容 std::allocator

由于内存片大小固定,不涉及最合适大小匹配问题,但插入、移除操作频繁而查找较少,所以选用链表结构来管理内存片。

MemoryPool 中有 2 个链表,都是双向链表(设计成双向链表主要是为了在移除指定元素时,能快速定位该元素的前后元素,保证链表完整性):

  • data_element_:记录已分配出去的内存片。
  • free_element_:记录未被分配出去的内存片。

① 内存块结构体(Block Structure):内存块结构体是内存池管理的基本单元,用来表示内存池中每一个可分配的内存块。

struct MemoryBlock {
    MemoryBlock* next; // 指向下一个内存块的指针,用于构建链表
    // 可以在这里添加其他元数据,如内存块的大小标识等
};

在这个结构体中,next 指针将各个内存块串联起来,形成链表结构。通过链表,内存池可以方便地管理空闲内存块和已分配内存块。比如在空闲链表中,每个节点都是 MemoryBlock 结构体,通过 next 指针可以快速找到下一个空闲块,当有分配请求时就能迅速取出一个空闲块。

② 空闲内存块链表(Free List):空闲内存块链表是内存池管理空闲内存的核心数据结构,由 MemoryBlock 结构体组成,链表中每个节点代表一个可用空闲内存块。

class MemoryPool {
private:
    MemoryBlock* freeList; // 空闲内存块链表的头指针
    // 其他成员变量和函数...
};

freeList 作为链表头指针,指向第一个空闲内存块。内存池初始化时,会将预先分配的内存块逐一加入链表,使它们成为可用空闲资源。分配时,从 freeList 指向的链表头部取出一个内存块分配给用户,然后更新 freeList 指向下一个空闲块;释放时,被释放的内存块重新插入链表头部,方便下次分配。

(2) 分配与释放流程

① 内存分配流程:当程序向内存池请求分配内存时,内存池首先检查空闲链表 freeList 是否为空。如果不为空,直接从链表头部取出一个内存块返回,同时更新 freeList 指针。

void* MemoryPool::allocate() {
    if (freeList == nullptr) {
        expandPool(); // 内存不足,扩展内存池
    }
    MemoryBlock* block = freeList;
    freeList = freeList->next;
    return block;
}

如果 freeList 为空,意味着当前没有空闲内存块可供分配,需要扩展内存池。通常是向操作系统申请一块新的更大的内存区域,然后将新内存按块大小划分,并加入到空闲链表中供后续分配。

② 内存释放流程:程序使用完内存并释放回内存池时,内存池将释放的内存块重新插入到空闲链表 freeList 头部,使其成为可复用资源。

void MemoryPool::deallocate(void* ptr) {
    MemoryBlock* block = static_cast<MemoryBlock*>(ptr);
    block->next = freeList;
    freeList = block;
}

这个过程就像将归还的商品重新摆放在货架显眼位置,方便下次快速取用。通过这种方式,内存池实现了内存的循环利用,显著提升了使用效率。

(3) 线程安全机制

在多线程环境下,内存池面临数据竞争和不一致的问题。多个线程同时访问和操作内存池时,可能出现一个线程在读取空闲链表时另一个线程正在修改,导致数据不准确,甚至出现内存块被重复分配或释放等错误。

常见的线程安全实现方法有以下几种:

① 互斥锁(Mutex):互斥锁保证同一时刻只有一个线程能访问内存池的关键数据结构和操作。在 C++ 中可以使用 std::mutex 实现。

class MemoryPool {
private:
    std::mutex mtx;
    MemoryBlock* freeList;
public:
    void* allocate() {
        std::lock_guard<std::mutex> lock(mtx);
        // 分配内存的逻辑
    }
    void deallocate(void* ptr) {
        std::lock_guard<std::mutex> lock(mtx);
        // 释放内存的逻辑
    }
};

allocatedeallocate 函数中,std::lock_guard<std::mutex> lock(mtx); 构造时自动锁定互斥锁,析构时自动解锁,确保函数执行期间其他线程无法同时访问内存池,避免数据竞争。

② 无锁数据结构(Lock-Free Data Structures):利用原子操作和特殊算法实现多线程安全,避免锁带来的开销。例如使用 std::atomic 类型实现无锁链表,通过原子操作保证链表指针修改的原子性。但无锁数据结构的实现较为复杂,需要对原子操作和并发编程有深入理解。

(4) 实战演练:代码示例

下面来看一个简单的固定内存池代码示例。这个内存池专门用于分配固定大小的内存块,通过链表管理空闲内存块。

#include <iostream>
#include <cstdlib>
#include <cassert>
// 定义内存块结构体
struct MemoryBlock {
    MemoryBlock* next; // 指向下一个内存块的指针
};
class MemoryPool {
public:
    MemoryPool(size_t blockSize, size_t initialBlocks)
        : blockSize(blockSize), initialBlocks(initialBlocks) {
        // 初始化内存池
        initializePool();
    }
    ~MemoryPool() {
        // 释放内存池中的所有内存
        MemoryBlock* current = poolStart;
        while (current) {
            MemoryBlock* next = current->next;
            free(current);
            current = next;
        }
    }
    void* allocate() {
        if (!freeList) {
            // 空闲链表为空,扩展内存池
            expandPool();
        }
        MemoryBlock* block = freeList;
        freeList = freeList->next;
        return block;
    }
    void deallocate(void* ptr) {
        MemoryBlock* block = static_cast<MemoryBlock*>(ptr);
        block->next = freeList;
        freeList = block;
    }
private:
    void initializePool() {
        // 申请初始内存块并构建空闲链表
        poolStart = static_cast<MemoryBlock*>(malloc(blockSize * initialBlocks));
        assert(poolStart != nullptr);
        freeList = poolStart;
        MemoryBlock* current = poolStart;
        for (size_t i = 1; i < initialBlocks; ++i) {
            current->next = static_cast<MemoryBlock*>(reinterpret_cast<char*>(current) + blockSize);
            current = current->next;
        }
        current->next = nullptr;
    }
    void expandPool() {
        // 扩展内存池,每次新增10个内存块
        size_t newBlocks = 10;
        MemoryBlock* newBlock = static_cast<MemoryBlock*>(malloc(blockSize * newBlocks));
        assert(newBlock != nullptr);
        MemoryBlock* current = newBlock;
        for (size_t i = 1; i < newBlocks; ++i) {
            current->next = static_cast<MemoryBlock*>(reinterpret_cast<char*>(current) + blockSize);
            current = current->next;
        }
        current->next = freeList;
        freeList = newBlock;
    }
    size_t blockSize; // 每个内存块的大小
    size_t initialBlocks; // 初始内存块数量
    MemoryBlock* poolStart; // 内存池起始地址
    MemoryBlock* freeList; // 空闲内存块链表头指针
};
// 示例使用
int main() {
    MemoryPool pool(16, 5); // 每个内存块16字节,初始5个内存块
    void* ptr1 = pool.allocate();
    void* ptr2 = pool.allocate();
    pool.deallocate(ptr1);
    pool.deallocate(ptr2);
    return 0;
}

MemoryBlock 结构体:定义了内存块的结构,包含指向下一个内存块的 next 指针,用于构建链表。

MemoryPool 类:内存池的核心类,包含以下成员:

  • 构造函数MemoryPool(size_t blockSize, size_t initialBlocks),接收每个内存块大小和初始内存块数量,调用 initializePool 初始化内存池。

  • 析构函数~MemoryPool(),释放内存池中所有申请的内存。

  • allocate 函数:从空闲链表中取出一个内存块分配给用户。如果空闲链表为空,则调用 expandPool 扩展内存池。

  • deallocate 函数:将用户释放的内存块重新插入到空闲链表头部。

  • initializePool 函数:初始化时一次性向操作系统申请 blockSize * initialBlocks 大小的内存,将这些内存块串联成空闲链表,freeList 指向链表头节点。

  • expandPool 函数:空闲链表为空时调用,每次扩展新增 10 个内存块,将新申请的内存块加入空闲链表头部。

  • main 函数:演示了内存池的基本使用,先分配两个内存块,然后释放它们。

通过这个示例可以清楚看到,内存池通过预先分配内存和管理空闲链表,实现了高效的内存分配与释放,减少了内存碎片和系统调用开销。




上一篇:UTFS轻量嵌入式存储:24字节头部替代手搓偏移,两个C源文件适配EEPROM
下一篇:C++内存池设计实现与性能优化:从原理到开源库对比
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-8-26 01:49 , Processed in 1.124892 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表