定长内存池

一、思路

首先开辟一个大块的连续内存,然后每次使用从大块内存中取出固定长度的小块内存。总体分为两部分:New和Delete。

New分为两种情况

①使用过的小块内存被释放了,我们使用链表将其管理起来,当再次调用New函数的时候,我们优先使用已经被释放的小块内存。

②释放列表为空,这时候又分为两种情况:

Ⅰ第一次调用New:直接使用malloc开辟一大块内存,然后再在这一大块内存中切割出一个小块内存,供人使用并返回。

Ⅱ大块内存剩余部分大于将要切割出的小块内存:直接在大块内存中切割出一小块内存并返回指针。

Ⅲ大块内存剩余部分小于将要切割出的小块内存:舍弃剩余部分,再开辟一个大块内存。

delete也分为两种情况

①释放链表为空:将被释放的小块内存的 next 指针置为nullptr,并将链表头指向该块。

②释放链表非空:将被释放的小块内存作为新的链表头,它的 next 指向原链表头(头插)。

二、实现

1.私有成员

cpp 复制代码
private:
	char* _memory = nullptr; //指向大块内存的指针,因为char是一个字节,好切
	size_t _remainBytes = 0; //大块内存在切分过程中剩余字节数
	void* deletelist = nullptr; //指向已经删除的内存空间链表

①成员含义

Ⅰ _memory:指向大块内存

Ⅱ _remainBytes:大块内存在切分过程中剩余的字节数

Ⅲ _deletelist:指向释放链表的表头

②类型说明

Ⅰ memory:使用char*是为了按字节精确切分大块内存

Ⅱ deletelist:为了做成通用空闲链表,不绑定具体类型

2.New函数实现

①首先实现释放链表为空的情况

cpp 复制代码
if (_remainBytes < sizeof(T))
{
	//开辟大块内存
	_remainBytes = 128 * 1024; //开一个128kb的空间
	_memory = (char*)malloc(_remianBytes);
}
//将小块内存切分出去,memory指向下一块小块内存,remainBytes减少sizeof(T)
T* obj = _memccpy;
_memory += sizeof(T);
_remainBytes -= sizeof(T);

return obj;

思路:当剩余部分不足以在切割出一个小块内存的时候,这种情况要么是第一次使用New要么就是空间用完了,这时候直接使用malloc开辟一块大内存,然后再移动memory并修改剩余字节数;如果当前剩余空间还能够切割出小块内存,则直接不走开辟大块内存的过程,直接走下边的逻辑即可。这个过程直接将释放链表为空中的三种情况融合到了一段代码中。

②释放链表不为空

cpp 复制代码
if(deletelist != nullptr)
{
	//思路:将表头给obj,将表头的next给deletlist,返回obj
	void* next = *((void**)_deletelist);
	obj = (T*)_deletelist;
	_deletelist = next;

    return obj;
}

思路:将表头给obj,将表头的next给deletelist,返回obj

注意:next获取的为什么这么写后边详细讲。

3.Delete函数实现

思路:有两种情况,链表为空和非空,由于链表刚开始就指向空,所以不管是链表为空还是非空,只需要将新的结点头插到链表中即可,所以两种情况在实现上的代码是相同的。

cpp 复制代码
void Delete(T* obj)
{
	*(void**)obj = _deletelist;
	_deletelist = obj;
}

为什么要使用*(void**)进行类型转换?

答:obj 这块内存的前几个字节,当成一个 "指针变量" 来用,存链表的 next 指针

(void**)obj 的意思是:

把 obj 这块内存,当成一个用来存放 "指针" 的变量

三、优化

1.如果T是char类型或者占的字节大小是小于我们需要的在里边插入的指针的大小,怎么办?

我们的思路是在分割内存的时候直接给这块小内存块分一块等于指针大小的内存块,比如你是char类型,按原来的逻辑需要给你分一块1字节的内存块,但是我现在给你一块指针大小的内存块,以确保你可以存储下一个指针。
实现:只需更改new函数中切割内存块的过程即可。

cpp 复制代码
if (_remainBytes < sizeof(T))
{
	//开辟大块内存
	_remainBytes = 128 * 1024; //开一个128kb的空间
	_memory = (char*)malloc(_remianBytes);
}
//将小块内存切分出去,memory指向下一块小块内存,remainBytes减少sizeof(T)
T* obj = _memccpy;
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objSize;
_remainBytes -= objSize;

2.显示析构删除结点

cpp 复制代码
void Delete(T* obj)
{
	obj->~T();

	*(void**)obj = _deletelist;
	_deletelist = obj;
}

3.定位new

普通 new = 开内存 + 调用构造函数

定位 new = 只调用构造函数,不开辟新内存

我们内存池,只是通过 malloc/ 指针偏移拿到了一块裸内存。 这块内存:

  • 地址合法、可以读写
  • 但没有初始化对象、没有执行构造函数、成员都是随机脏值

new(obj) T(); 的作用: 在 已经分配好的 obj 内存地址上,手动执行一遍 T 的构造函数,把裸内存初始化为合法的 T 对象。
为什么必须写这一行?不写会崩 / 出 bug

  1. 裸内存无对象状态 malloc 只会申请原始内存,不会构造对象,类的成员变量都是随机脏数据。

  2. 析构会出问题 你的 Delete 函数调用了 obj->~T() 析构函数。 如果没有构造就直接析构 ,属于未定义行为,大概率程序崩溃。

  3. 自定义类型必崩 如果是 int 这种基础类型,偶尔能跑; 如果是 string、自定义结构体、带资源的类,百分百内存泄漏 + 崩溃

和普通new的区别

cpp 复制代码
T* p = new T();   // 1. 堆上开辟内存  2. 构造对象
new(obj) T();     // 1. 不开辟内存    2. 仅构造对象
cpp 复制代码
else
{
	if (_remainBytes < sizeof(T))
	{
		//开辟大块内存
		_remainBytes = 128 * 1024; //开一个128kb的空间
		_memory = (char*)malloc(_remainBytes);
		if (_memory == nullptr)
			throw std::bad_alloc();
	}
	//将小块内存切分出去,memory指向下一块小块内存,remainBytes减少sizeof(T)
	obj = (T*)_memory;
	size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
	_memory += objSize;
	_remainBytes -= objSize;

}
new(obj)T;
return obj;
}

四、内存池的意义

控制台测试 new / 内存池效果一模一样,我为什么要费这么大劲写内存池?直接 int* 定义、直接 new 不行吗?

  1. 控制台场景:确实没用

如果只是:

  • 创建一两个 int 对象

  • 简单赋值、打印

  • 程序运行一秒就结束

new 完全够用,内存池毫无优势。

  1. 真实工程场景:new 根本扛不住

new / malloc 是操作系统全局堆,存在三个致命问题:

  • 速度慢:每次 new 都是系统调用、堆查找、加锁解锁

  • 内存碎片严重:频繁小块分配释放,导致内存碎片化,大内存申请失败

  • 高并发锁竞争:全局堆多线程争抢锁,性能暴跌

  1. 内存池的核心优势(真正的设计意义)
  • 预分配大块内存:只调用一次 malloc,后续分配只是指针挪动 O(1)

  • 复用释放的内存:自由链表缓存释放的对象,无需反复系统调用

  • 无内存碎片:统一大块管理,不会产生细碎碎片

  • 高性能高并发:用户态内存管理,无全局锁竞争

两个关键类型的设计精髓?

  1. 为什么大块内存用 char*?

char 占 1 字节,指针加减精准按字节移动,完美实现任意大小内存切割。

如果用 int*/void* 都无法精准分步切割内存。

  1. 为什么自由链表要用 void** 强转?

*(void**)obj = _deletelist;

  • (void**)obj:把对象内存起始地址, reinterpret 为「指针变量的地址」

  • * 解引用:操作这块内存,写入 next 指针

核心思想:利用已释放对象自身的内存,存储链表指针,零额外开销
总结​

  1. 内存池不是为控制台小demo设计的,是为高频率、大批量、长期运行的程序设计的(游戏、服务器、后端服务)。​

  2. 直接 new 写代码简单,但性能差、碎片多、并发弱。​

  3. 定长内存池核心:预分配大块 + 指针切割 + 自由链表复用。​

  4. 手写内存池能彻底搞懂:内存对齐、指针强转、自由链表、对象构造析构、内存碎片等底层核心知识。

五、代码展示

cpp 复制代码
#pragma once
#include <iostream>

template <class T> //类型
class ObjectPoll
{
public:
	T* New()
	{
		T* obj = nullptr;
		if(_deletelist != nullptr)
		{
			//思路:将表头给obj,将表头的next给deletlist,返回表头
			void* next = *((void**)_deletelist);
			obj = (T*)_deletelist;
			_deletelist = next;
		}
		else
		{
			if (_remainBytes < sizeof(T))
			{
				//开辟大块内存
				_remainBytes = 128 * 1024; //开一个128kb的空间
				_memory = (char*)malloc(_remainBytes);
				if (_memory == nullptr)
					throw std::bad_alloc();
			}
			//将小块内存切分出去,memory指向下一块小块内存,remainBytes减少sizeof(T)
			obj = (T*)_memory;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objSize;
			_remainBytes -= objSize;

		}
		new(obj)T;
		return obj;
		}
	//思路:头插
	void Delete(T* obj)
	{
		obj->~T();

		*(void**)obj = _deletelist;
		_deletelist = obj;
	}
private:
	char* _memory = nullptr; //指向大块内存的指针,因为char是一个字节,好切
	size_t _remainBytes = 0; //大块内存在切分过程中剩余字节数
	void* _deletelist = nullptr; //指向已经删除的内存空间链表
};

感谢观看

相关推荐
零点一顿微胖1 小时前
[Agent] 初始化Agent服务 Rust版
开发语言·网络·rust
鹿鸣天涯1 小时前
网规第三版:第9章网络安全部署案例
网络·安全·web安全·软考·网络规划设计师
艾莉丝努力练剑1 小时前
【Linux网络】Linux 网络编程:传输层TCP(四)
linux·运维·服务器·网络·tcp/ip·http
七夜zippoe1 小时前
DolphinDB异常检测引擎:实时告警
java·服务器·网络·异常·告警·dolphindb
程序猿编码1 小时前
如何把远程文件变化“骗“成本地inotify事件:一个LD_PRELOAD钩子
c语言·开发语言·网络·tcp/ip·安全
段一凡-华北理工大学10 小时前
2026 高炉炼铁智能化技术全景与演进路径~系列文章11:演进路径与行业未来
大数据·网络·人工智能·算法·工业智能体·高炉炼铁智能化
leoFY12312 小时前
STM32H750配置LAN PHY芯片LAN8742
网络·stm32·嵌入式硬件
阿部多瑞 ABU13 小时前
AI红队攻防演化史(2023-2026):从虚拟角色到RLHF劫持——所有攻击方法全景总结与最新趋势分析
网络·人工智能·安全
博客-小覃13 小时前
Zabbix之华为交换机的日志记录信息操作详细教程
服务器·网络·华为·zabbix