C++学习记录:内存池设计与实现 及其详细代码

C++学习记录:内存池设计与实现 及其详细代码

  这是我在VS2019上写的第一个项目,使用VS2019的目的是想在更为规范的IDE上写出更加规范的代码。
  使用内存池可以减少程序运行中产生的内存碎片,且可以提高程序内存分配效率从而提升程序效率。在这篇笔记中,我将记录下自己关于这个内存池项目的思路与详细代码。同时,在我的C++网络编程学习相关内容的下一步改进中,我将引入这个内存池提高服务端的运行效率。


一、内存池设计思路

  首先,为什么要使用内存池?
  我是这样理解的:不断的使用new/malloc从堆中申请内存,会在内存中留下一些“缝隙”。例如我们申请三份8个字节大小的内存A、B、C,由于内存地址是连续的,则ABC的地址值每个相差8(正常情况)。此时我们delete/free掉B内存,A与C内存之间此时就有了8个字节的空白。假如我们今后申请的内存都比8个字节大,则A与C之间这块内存就会一直为空白,这就是内存碎片。
  过多的内存碎片会影响程序的内存分配效率,为了降低内存碎片的影响,我们可以引入内存池来尝试解决它。

  我们可以在程序启动时(或是其他合适的时机),预先申请足够的、大小相同的内存,把这些内存放在一个容器内。在需要申请内存时,直接从容器中取出一块内存使用;而释放内存时,把这块内存放回容器中即可。这个容器就被称为内存池。而这样操作也可以大大减少内存碎片出现的可能性,提高内存申请/释放的效率。


这个项目中内存池的思路图如下:
思路图
我们需要新建三个类:

  • 首先是底层的内存块类,其中包含了该内存块的信息:内存块编号、引用情况、所属内存池、下一块的位置等。
  • 其次是内存池类,它对成组的内存块进行管理,可以实现把内存块从内存池中取出以及把内存块放回内存池
  • 最后是内存管理工具类,其中包含一个或多个内存池,所以它要根据用户申请的内存大小找到合适的内存池,调用内存池类的方法申请/释放内存。

还需要进行的操作:

  • new/delete进行重载,使其直接调用内存管理工具类申请/释放内存。

  上面的工作完成后,我们仍是以new/delete来申请/释放内存,但是已经是通过内存池来实现的了,这个内存池项目也就暂时结束。下面我将详细记录实现的过程与思路。

二、内存块类MemoryBlock 设计与实现

先扔出来思路图:
思路图
  首先,在内存池中每一块内存是由一个内存头以及其可用内存组成的,其中内存头里储存了这块内存的相关信息,可用内存即为数据域,类似链表中节点的结构。而一块块内存之间正是一种类似链表的结构,即通过内存头中的一个指针进行连接。内存头中包含的信息大概如下:

  • 1、内存块编号
  • 2、引用情况
  • 3、所属内存池
  • 4、下一块位置
  • 5、是否在内存池内

则我们可以通过上面的思路新建内存块类MemoryBlock

由于内存头中要标记所属内存池,所以我们先预声明内存池类,在之后再进行实现。
建立完成后,内存池内一块内存的大小为:sizeof(MemoryBlock) + 可用内存的大小

//预声明内存池类
class MemoryAlloc;
//内存块类
class MemoryBlock
{
public:
	//内存块编号
	int _nID;
	//引用情况
	int _nRef;
	//所属内存池
	MemoryAlloc* _pAlloc;
	//下一块位置
	MemoryBlock* _pNext;
	//是否在内存池内
	bool _bPool;

private:

};

三、内存池类MemoryAlloc 设计与实现

还是先扔出来内存池申请/释放内存的思路图:
思路图
  由图可知,整个内存池的管理基本为链表结构,内存池对象一直指向头部内存单元。在申请内存时移除头部单元,类似链表头结点的移除;在释放内存时,类似链表的头插法,把回收回来的内存单元放在内存池链表的头部。

内存池类中大概包含这些东西:

1、方法

  • 1.成员变量初始化 —— 对内存单元可用内存大小以及内存单元数量进行设定
  • 2.初始化 —— 依据内存单元的大小与数量,对内存池内的内存进行malloc申请,完善每一个内存单元的信息
  • 3.申请内存 —— 从内存池链表中取出一块可用内存
  • 4.释放内存 —— 将一块内存放回内存池链表中

2、成员变量

  • 1.内存池地址 —— 指向内存池内的总内存
  • 2.头部内存单元 —— 指向头部内存单元
  • 3.内存块大小 —— 内存单元的可用内存大小
  • 4.内存块数量 —— 内存单元的数量

则我们可以通过上面的思路新建内存块类MemoryAlloc

//导入内存块头文件
#include"MemoryBlock.h"

class MemoryAlloc
{
public:
	MemoryAlloc();
	virtual ~MemoryAlloc();
	//设置初始化
	void setInit(size_t nSize,size_t nBlockSize);//传入的为内存块可用内存大小和内存块数量
	//初始化
	void initMemory();
	//申请内存
	void* allocMem(size_t nSize);//传入的为申请可用内存的大小
	//释放内存
	void freeMem(void* p);

protected:
	//内存池地址
	char* _pBuf;
	//头部内存单元
	MemoryBlock* _pHeader;
	//内存块大小
	size_t _nSize;
	//内存块数量
	size_t _nBlockSize;
	//多线程锁
	std::mutex _mutex;//锁上申请内存方法和释放内存方法即可实现多线程操作
};

四、内存管理工具类MemoryMgr 设计与实现

仍然是先放思路图:
思路图
  首先,内存管理工具类用的是单例对象模式,从而能简易的对内存池进行管理。在这次的实现里,我使用的是饿汉式单例对象。其次,为了更简单的判断出申请内存时所需要调用的内存池,我建立了一个数组映射内存池。在工具类构造函数内,首先是对内存池进行初始化,随后便是将其映射到数组上。

映射:
假如申请一个64字节内存池,申请一个128字节内存池
我们新建一个指针数组test,使下标0~64指向64字节内存池,下标65~128指向128字节内存池
则我们通过 test[要申请的内存大小] 即可确定合适的内存池

  在随后的申请过程中,我们首先判断申请内存大小是否超过内存池最大可用内存,若没超过,则通过映射数组指向的内存池进行内存申请;若超过了,则直接使用malloc申请,记得多申请一个内存头大小的内存。随后完善内存头内的资料。
  在随后的释放过程中,我们通过内存头判断这块内存是否使属于内存池的内存,如果是,则通过其所属内存池进行内存回收;若不是,则直接进行free释放。

内存管理工具类中大概包含这些东西:

1、方法

  • 饿汉式单例模式 —— 调用返回单例对象
  • 申请内存 —— 调用获取一块内存
  • 释放内存 —— 调用释放一块内存
  • 内存初始化 —— 将内存池映射到数组上

2、成员变量

  • 映射数组 —— 映射内存池
  • 内存池1
  • 内存池2
  • 内存池…

则我们可以通过上面的思路新建内存管理工具类MemoryMgr

//内存池最大申请
#define MAX_MEMORY_SIZE 128

//导入内存池模板类
#include"MemoryAlloc.h"

class MemoryMgr
{
public:
	//饿汉式单例模式
	static MemoryMgr* Instance();
	//申请内存
	void* allocMem(size_t nSize);
	//释放内存
	void freeMem(void* p);
	
private:
	MemoryMgr();
	virtual ~MemoryMgr();
	//内存映射初始化
	void init_szAlloc(int begin,int end,MemoryAlloc* pMem);

private:
	//映射数组
	MemoryAlloc* _szAlloc[MAX_MEMORY_SIZE + 1];
	//64字节内存池
	MemoryAlloc _mem64;
	//128字节内存池
	MemoryAlloc _mem128;
	//内存池...
};

五、重载new/delete

重载new/delete就不多说了,直接放代码:

void* operator new(size_t size);
void operator delete(void* p);
void* operator new[](size_t size);
void operator delete[](void* p);
void* mem_alloc(size_t size);//malloc
void mem_free(void* p);//free

六、项目代码及其注释

1.项目图片

图片

2.重载new/delete

2.1 Alloctor.h

#ifndef _Alloctor_h_
#define _Alloctor_h_

void* operator new(size_t size);
void operator delete(void* p);
void* operator new[](size_t size);
void operator delete[](void* p);
void* mem_alloc(size_t size);
void mem_free(void* p);

#endif

2.2 Alloctor.cpp

#include"Alloctor.h"
#include"MemoryMgr.h"//内存管理工具

void* operator new(size_t size)
{
	return MemoryMgr::Instance()->allocMem(size);
}

void operator delete(void* p)
{
	MemoryMgr::Instance()->freeMem(p);
}

void* operator new[](size_t size)
{
	return MemoryMgr::Instance()->allocMem(size);
}

void operator delete[](void* p)
{
	MemoryMgr::Instance()->freeMem(p);
}

void* mem_alloc(size_t size)
{
	return MemoryMgr::Instance()->allocMem(size);
}

void mem_free(void* p)
{
	MemoryMgr::Instance()->freeMem(p);
}

3.内存池类MemoryAlloc

3.1 MemoryAlloc.h

/*
内存池类
对内存块进行管理
2021/2/26
*/

#ifndef Memory_Alloc_h
#define Memory_Alloc_h

//导入内存块头文件
#include"MemoryBlock.h"

class MemoryAlloc
{
public:
	MemoryAlloc();
	virtual ~MemoryAlloc();
	//设置初始化
	void setInit(size_t nSize,size_t nBlockSize);
	//初始化
	void initMemory();
	//申请内存
	void* allocMem(size_t nSize);
	//释放内存
	void freeMem(void* p);

protected:
	//内存池地址
	char* _pBuf;
	//头部内存单元
	MemoryBlock* _pHeader;
	//内存块大小
	size_t _nSize;
	//内存块数量
	size_t _nBlockSize;
	//多线程锁
	std::mutex _mutex;
};

#endif

3.2 MemoryAlloc.cpp

#include"MemoryAlloc.h"

MemoryAlloc::MemoryAlloc()
{
	_pBuf = nullptr;
	_pHeader = nullptr;
	_nSize = 0;
	_nBlockSize = 0;
}

MemoryAlloc::~MemoryAlloc()
{
	if (_pBuf)
	{
		free(_pBuf);
		//现在有一个问题就是内存池外申请的内存不会被主动释放
	}
}

void MemoryAlloc::setInit(size_t nSize, size_t nBlockSize)
{
	/*补全nSize
	const size_t n = sizeof(void*)
	_nSize = (nSize/n) * n + (nSize % n ? n : 0);
	*/
	_pBuf = nullptr;
	_pHeader = nullptr;
	_nSize = nSize;
	_nBlockSize = nBlockSize;
	initMemory();
}

void MemoryAlloc::initMemory()
{
	//断言
	assert(nullptr == _pBuf);
	//若已申请则返回
	if (nullptr != _pBuf)
	{
		return;
	}
	//计算内存池的大小  (块大小+块头) * 块数量
	size_t temp_size = _nSize + sizeof(MemoryBlock);//需要偏移的真正大小
	size_t bufSize = temp_size * _nBlockSize;
	//向系统申请池内存
	_pBuf = (char*)malloc(bufSize);
	//初始化内存池
	_pHeader = (MemoryBlock*)_pBuf;
	if (nullptr != _pHeader)
	{
		_pHeader->_bPool = true;//在池中
		_pHeader->_nID = 0;//第0块
		_pHeader->_nRef = 0;//引用次数为0
		_pHeader->_pAlloc = this;//属于当前内存池
		_pHeader->_pNext = nullptr;//下一块
		MemoryBlock* pTemp1 = _pHeader;
		//遍历内存块进行初始化
		for (size_t n = 1; n < _nBlockSize; n++)
		{
			MemoryBlock* pTemp2 = (MemoryBlock*)(_pBuf + (n * temp_size));//指针偏移到下一块
			pTemp2->_bPool = true;//在池中
			pTemp2->_nID = n;//第n块
			pTemp2->_nRef = 0;
			pTemp2->_pAlloc = this;
			pTemp2->_pNext = nullptr;
			pTemp1->_pNext = pTemp2;
			pTemp1 = pTemp2;
		}
	}
}

void* MemoryAlloc::allocMem(size_t nSize)
{
	//自解锁
	std::lock_guard<std::mutex> lock(_mutex);
	//若内存池不存在则初始化
	if (nullptr == _pBuf)
	{
		initMemory();
	}
	MemoryBlock* pReturn = nullptr;
	if (nullptr == _pHeader)//如内存池已满 重新申请
	{
		pReturn = (MemoryBlock*)malloc(nSize+sizeof(MemoryBlock));
		if (nullptr != pReturn)
		{
			pReturn->_bPool = false;//不在池中
			pReturn->_nID = -1;
			pReturn->_nRef = 1;
			pReturn->_pAlloc = this;
			pReturn->_pNext = nullptr;
		}
	}
	else//否则直接使用内存池
	{
		pReturn = _pHeader;
		_pHeader = _pHeader->_pNext;
		assert(0 == pReturn->_nRef);
		pReturn->_nRef = 1;
	}
	//debug打印
	if (nullptr != pReturn)
	{
		xPrintf("NEW - allocMem:%p,id=%d,size=%d\n", pReturn, pReturn->_nID, nSize);
	}	
	return ((char*)pReturn + sizeof(MemoryBlock));
}

void MemoryAlloc::freeMem(void* p)
{
	//传进来的是消息区 需要加上信息头
	MemoryBlock* pBlock = (MemoryBlock*)((char*)p - sizeof(MemoryBlock));
	assert(1 == pBlock->_nRef);
	//判断是否被多次引用
	if (--pBlock->_nRef != 0)
	{
		return;
	}
	//判断是否在内存池中
	if (pBlock->_bPool)
	{
		//自解锁
		std::lock_guard<std::mutex> lock(_mutex);
		//把内存块放入内存池首位
		pBlock->_pNext = _pHeader;
		_pHeader = pBlock;
	}
	else
	{
		free(pBlock);
	}
}

4.内存块类MemoryBlock

4.1 MemoryBlock.h

/*
内存块类
内存管理的最小单位
2021/2/26
*/

#ifndef Memory_Block_h
#define Memory_Block_h

//声明内存池类
class MemoryAlloc;
//最底层导入内存头文件/断言头文件/锁头文件
#include<stdlib.h>
#include<assert.h>
#include<mutex>
//如果为debug模式则加入调试信息
#ifdef _DEBUG
	#include<stdio.h>
	#define xPrintf(...) printf(__VA_ARGS__)
#else
	#define xPrintf(...)
#endif
 
class MemoryBlock
{
public:
	//内存块编号
	int _nID;
	//引用情况
	int _nRef;
	//所属内存池
	MemoryAlloc* _pAlloc;
	//下一块位置
	MemoryBlock* _pNext;
	//是否在内存池内
	bool _bPool;

private:

};

#endif

4.2 MemoryBlock.cpp

#include"MemoryBlock.h"

5.内存管理工具类MemoryMgr

5.1 MemoryMgr.h

/*
内存管理工具类
对内存池进行管理
2021/2/26
*/

#ifndef Memory_Mgr_h
#define Memory_Mgr_h
//内存池最大申请
#define MAX_MEMORY_SIZE 128

//导入内存池模板类
#include"MemoryAlloc.h"

class MemoryMgr
{
public:
	//饿汉式单例模式
	static MemoryMgr* Instance();
	//申请内存
	void* allocMem(size_t nSize);
	//释放内存
	void freeMem(void* p);
	//增加内存块引用次数
	void addRef(void* p);

private:
	MemoryMgr();
	virtual ~MemoryMgr();
	//内存映射初始化
	void init_szAlloc(int begin,int end,MemoryAlloc* pMem);

private:
	//映射数组
	MemoryAlloc* _szAlloc[MAX_MEMORY_SIZE + 1];
	//64字节内存池
	MemoryAlloc _mem64;
	//128字节内存池
	MemoryAlloc _mem128;
};

#endif

5.2 MemoryMgr.cpp

#include"MemoryMgr.h"

MemoryMgr::MemoryMgr()
{
	_mem64.setInit(64, 10);
	init_szAlloc(0, 64, &_mem64);
	_mem128.setInit(128, 10);
	init_szAlloc(65, 128, &_mem128);
}

MemoryMgr::~MemoryMgr()
{
}

//初始化
void MemoryMgr::init_szAlloc(int begin, int end, MemoryAlloc* pMem)
{
	//begin到end大小的内存申请都映射到相关的内存池上
	for (int i = begin; i <= end; i++)
	{
		_szAlloc[i] = pMem;
	}
}

//饿汉式单例模式
MemoryMgr* MemoryMgr::Instance()
{
	static MemoryMgr myMemoryMgr;
	//单例对象
	return &myMemoryMgr;
}

//申请内存
void* MemoryMgr::allocMem(size_t nSize)
{
	//若申请的内存大小正常,则直接申请
	if (nSize <= MAX_MEMORY_SIZE)
	{
		return _szAlloc[nSize]->allocMem(nSize);
	}
	else//否则用malloc申请一个
	{
		MemoryBlock* pReturn = (MemoryBlock*)malloc(nSize + sizeof(MemoryBlock));
		if (nullptr != pReturn)
		{
			pReturn->_bPool = false;//不在池中
			pReturn->_nID = -1;
			pReturn->_nRef = 1;
			pReturn->_pAlloc = nullptr;
			pReturn->_pNext = nullptr;
			//debug打印
			xPrintf("NEW - allocMem:%p,id=%d,size=%d\n",pReturn,pReturn->_nID,nSize);
		}
		return ((char*)pReturn + sizeof(MemoryBlock));
	}
}

//释放内存
void MemoryMgr::freeMem(void* p)
{
	//传进来的是消息区 需要加上信息头
	MemoryBlock* pBlock = (MemoryBlock*)((char*)p - sizeof(MemoryBlock));
	//debug打印
	xPrintf("DELETE - allocMem:%p,id=%d\n", pBlock, pBlock->_nID);
	//内存池内的内存块/内存池外的内存块 不同的处理方式
	if (pBlock->_bPool == true)
	{
		pBlock->_pAlloc->freeMem(p);
	}
	else
	{
		if (--pBlock->_nRef == 0)
		{
			free(pBlock);
		}
	}
}

//增加内存块引用次数
void MemoryMgr::addRef(void* p)
{
	MemoryBlock* pBlock = (MemoryBlock*)((char*)p - sizeof(MemoryBlock));
	++pBlock->_nRef;
}

6.main文件

6.1 main.cpp

#include<stdio.h>
#include<stdlib.h>
#include"Alloctor.h"

#ifdef _DEBUG
#endif

int main()
{
	char* data2 = new char;
	delete data2;

	char* data1 = new char[129];
	delete[] data1;

	char* data3 = new char[65];
	delete[] data3;

	printf("--------------------------\n");
	char* data[15];
	for (size_t i = 0; i < 12; i++)
	{
		data[i] = new char[64];
		delete[] data[i];
	}

	return 0;
}

七、小结

  • 在申请与释放内存时,返回给用户和用户传进来的都是可用内存的地址,并不是内存头的地址。我们需要对地址进行偏移,从而返回/接收正确的地址。具体为可用内存地址向前偏移一个内存头大小即为内存头地址;内存头地址向后偏移一个内存头大小即为可用内存地址。
  • 内存池初始化时,申请总地址大小为:(可用地址大小+内存头大小) * 内存单元数量
  • 内存池外申请的内存,不会在内存池析构函数内被释放,需要手动释放。(不过一般触发析构函数的时候,也不用手动释放了)
  • 在这次的项目中,我对地址、内存等有了更深刻的理解,同时也能熟练使用VS的调试功能。希望未来能有更大的发展。
  • 10
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值