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的调试功能。希望未来能有更大的发展。