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

一、为什么需要内存池?

常规的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或mmapvoid* ptr = mmap(nullptr, kpage * 4096, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
#endifif (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或mmapvoid* ptr = mmap(nullptr, kpage * 4096, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
#endifif (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) 的链表操作,仅在内存不足时触发一次系统调用
内存碎片严重。频繁分配 / 释放不同大小内存会产生大量细碎空闲块几乎无碎片。所有内存块大小统一,释放后可被同类型对象复用
适用场景低频次、大小不固定的内存分配(如偶尔创建大对象)高频次、同类型小对象的分配 / 释放(如链表节点、树节点)
额外内存开销有(系统需维护内存块元信息,如大小、边界标记)几乎无(利用对象自身内存存储自由链表指针)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/920831.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/920831.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【跨境电商】上中下游解释,以宠物行业为例

上中下游概念及其在宠物行业的应用 在产业链分析中&#xff0c;“上中下游”指的是一个产品或服务的不同环节&#xff1a;上游涉及原材料供应和基础资源&#xff0c;中游负责生产加工和制造&#xff0c;下游则包括销售、分销和服务。这种划分有助于理解整个价值链的运作。下面&…

飞牛NAS上部署Markdown文稿编辑器,阅读.md文件同时还可以跨平台访问!

前言前段时间小白在使用.md文件的阅读器&#xff0c;好像是什么*ypor*&#xff0c;但是这个软件它收费。&#xff08;也不是找不到PJ版本&#xff0c;只是感觉这是人家的知识产权&#xff0c;就不整了。&#xff09;于是小白在寻找能够代替这个软件的其他软件&#xff0c;而且如…

浅谈 SQL 窗口函数:ROW_NUMBER() 与聚合函数的妙用

在日常开发中&#xff0c;我们经常会遇到这样的需求&#xff1a;既要保留明细数据&#xff0c;又要对数据进行排名、累计、分区统计。如果仅依赖传统的 GROUP BY&#xff0c;往往需要做多次子查询或者复杂的 JOIN&#xff0c;既繁琐又低效。 而 窗口函数&#xff08;Window Fun…

DSPFilters实现低通滤波器(QT)

DSPFilters实现低通滤波器DSPFilters实现低通滤波器DSPFilters安装-构建静态库QT代码复制include和静态库到qt项目qt代码配置效果DSPFilters实现低通滤波器 https://github.com/vinniefalco/DSPFilters DSPFilters安装-构建静态库 用 Qt 自带的 MinGW&#xff08;最简单&…

mybatis plus 基本使用和源码解析

简介 mybatis-plus是一款mybatis增强工具&#xff0c;用于简化开发&#xff0c;提高效率。mybatis-plus免去了用户编写sql的麻烦&#xff0c;只需要创建好实体类&#xff0c;并创建一个继承自BaseMapper的接口&#xff0c;mybatis就可以自动生成关于单表的crud。mybatis-plus自…

【Android】Notification 的基本使用

文章目录【Android】Notification的基本使用权限通知的基本使用1. 获取通知管理器&#xff08;用于发送、更新、取消通知&#xff09;2. 创建通知渠道&#xff08;Android 8.0 必须&#xff09;3. 使用通知3.1 发送通知3.2 更新通知3.3 取消通知通知的进阶技巧通知显示样式1. B…

Web前端开发基础

1.前端概论 1.1 什么是前端&#xff1f; 概念&#xff1a;前端(Front-End)&#xff0c;也称为客户端(Client-Side)&#xff0c;指的是用户在使用网站或Web应用时直接看到并与之交互的部分。它涵盖了屏幕上的一切内容&#xff0c;从文字、图片、按钮、布局到动画效果 一个简单的…

并发编程——11 并发容器(Map、List、Set)实战及其原理分析

1 JUC包下的并发容器Java 基础集合&#xff08;如 ArrayList、LinkedList、HashMap&#xff09;非线程安全。为了解决线程安全问题&#xff0c;Java 最初提供了同步容器&#xff08;如 Vector、Hashtable、SynchronizedList&#xff09;&#xff0c;但它们通过 synchronized 实…

Circuitjs 测试点的使用

在电路中, 有时候我们想知道, 各个节点上电压的具体的值. 比如下面这个电路:电流流经两个电阻器之后, 电压在下降. 如果想知道具体节点电压的确切数值, 可以通过添加 测试点(Test Point) 实现. 点击 绘制–输出和标签–添加测试点, 之后在所需测量的节点上拖动添加一个测试点, …

Ansible Playbook 实践

Ansible Playbook 实践一、Playbook 基础规范&#xff08;一&#xff09;YAML 格式要求文件标识&#xff1a;以 --- 开头&#xff0c;明确为 YAML 文件&#xff0c;结尾可加 ...&#xff08;可选&#xff0c;用于标记文件结束&#xff09;。注释规则&#xff1a;用 # 实现注释&…

基于 Vue + Interact.js 实现可拖拽缩放柜子设计器

实现可视化设计工具已成为前端开发的重要挑战之一。本文将介绍如何使用 Vue.js 配合 Interact.js 库创建一个功能完整的橱柜设计器&#xff0c;兼容PC和移动终端。核心功能网格系统&#xff1a;基于 CSS 网格实现精准定位拖拽功能&#xff1a;实现单元格的自由移动缩放控制&…

今日科技速递 | 智能芯片突围、AI+行动深化、服贸会科技成果亮相

今日科技速递 | 智能芯片突围、AI行动深化、服贸会科技成果亮相 一、乐鑫科技涨停&#xff1a;Wi-Fi 6/7 与 AIoT 芯片双路径创新驱动 新闻回顾 2025 年 8 月 27 日&#xff0c;科创板公司 乐鑫科技&#xff08;688018&#xff09; 盘中一度涨停&#xff0c;股价达到 225 元&am…

PDF压缩如何平衡质量与体积?

在日常工作或者生活中&#xff0c;我们常常要处理PDF文档&#xff0c;很多人可能会遇到这样的困扰&#xff1f;使用WPS处理PDF时&#xff0c;部分功能需要付费&#xff0c;这给我们带来了许多不便。 它的使用方式十分简单&#xff0c;你只要双击图标&#xff0c;它就能启动&am…

Flask 之上下文详解:从原理到实战

一、引言&#xff1a;为什么 Flask 需要“上下文”&#xff1f;在 Web 开发中&#xff0c;我们经常需要访问当前请求的信息&#xff08;如 URL、表单数据&#xff09;、当前应用实例&#xff08;如配置、数据库连接&#xff09;或用户会话状态。传统做法是使用全局变量&#xf…

深入探索Vue:前端开发的强大框架

在当今的前端开发领域&#xff0c;Vue作为一款备受瞩目的JavaScript框架&#xff0c;以其简洁易用、高效灵活等特性&#xff0c;赢得了众多开发者的青睐。无论是构建小型的交互页面&#xff0c;还是开发大型的单页应用&#xff0c;Vue都能展现出卓越的性能和出色的表现。本文将…

B树与B+树的原理区别应用

在磁盘存储和内存有序的数据管理中&#xff0c;B 树与 B 树是核心的数据结构&#xff0c;二者均通过 “多路平衡” 特性减少 IO 次数&#xff0c;但在数据存储方式、查询逻辑上存在本质差异。一、B 树&#xff08;Balance Tree&#xff09;&#xff1a;多路平衡搜索树B 树是 “…

从零到一:使用anisble自动化搭建kubernetes集群

在我们云原生俱乐部的暑期学习中&#xff0c;我们了解并学习了需要关于云原生的技术&#xff0c;其中在应用层面上最重要的就是shell编程和ansible&#xff0c;而想要掌握这两项技术离不开的就是实践&#xff0c;而kubernetes是我们云原生技术栈的核心技术&#xff0c;在生产实…

【LangGraph】langgraph.prebuilt.create_react_agent() 函数:快速创建基于 ReAct(Reasoning + Acting)架构的智能代理

本文是对 langgraph.prebuilt.create_react_agent 函数的详细且全面的介绍&#xff0c;涵盖其定义、功能、设计理念、参数、返回值、使用场景、实现原理、示例代码、高级用法、注意事项、与其他方法的对比&#xff0c;以及学习建议。 1. 概述 langgraph.prebuilt.create_react…

北斗导航 | RAIM算法改进方案及性能对比分析报告

github&#xff1a;https://github.com/MichaelBeechan CSDN&#xff1a;https://blog.csdn.net/u011344545 文章目录RAIM算法改进方案及性能对比分析报告一、RAIM算法改进技术框架1.1 多假设分组算法&#xff08;MHSS&#xff09;1.2 动态噪声估计算法1.3 多源信息融合技术二、…

数据结构第8章 排序(竟成)

第 8 章 排序【考纲内容】1.排序的基本概念&#xff1b;2. 直接插入排序&#xff1b;3. 折半插入排序&#xff1b;4. 起泡排序&#xff08;Bubble Sort&#xff09;&#xff1b;5.简单选择排序&#xff1b;6. 希尔排序&#xff08;Shell Sort&#xff09;&#xff1b;7. 快速排…