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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
由于内存头中要标记所属内存池,所以我们先预声明内存池类,在之后再进行实现。
建立完成后,内存池内一块内存的大小为: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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//导入内存块头文件
#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 设计与实现

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

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

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

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

1、方法

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

2、成员变量

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//内存池最大申请
#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就不多说了,直接放代码:

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
9
10
11
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
内存池类
对内存块进行管理
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
内存块类
内存管理的最小单位
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

1
#include"MemoryBlock.h"

5.内存管理工具类MemoryMgr

5.1 MemoryMgr.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
内存管理工具类
对内存池进行管理
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#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的调试功能。希望未来能有更大的发展。