在软件开发中,为了避免内存碎片、避免频繁的内存申请,或是想更好的管理内存,内存池是一种解决方案。
本篇通过一个最简单的内存池,来介绍内存池的基本原理。
1 原理介绍
内存池,简单来说,就是预先向系统申请一批内存,后续在需要使用内存时,都从这一批的空闲内存中去申请,使用完毕后,也是将内存归还给这批内存,不再向系统申请和释放内存。
1.1 初始化内存池
内存池(MemPool)的管理,通常使用链表的形式,管理一个个内存块(MemBlock),内存池需要一个首地址,记录内存池的起始地址,然后内存块通过next指针连接,最后,需要记录最后的空闲内存块的指针(freelist)。
本篇的MemPool的数据结构如下:
- Block *freelist:记录空闲内存块的指针char *memory:记录内存池的地址
内存块Block,至少记录next指针,用于寻找下一个内存块。
User类是一个用于测试申请内存的类。
下图示例了一个有5个内存块的内存池,每个内存块的大小是User类的大小,占32字节。
1.2 申请使用内存
申请使用内存时,通过空闲指针freelist,如果是之前没有申请过,freelist也就是链表的最后一个内存块,即从最后一个内存块开始申请使用内存。
从内存池申请到内存后,空闲指针会启动到前一个内存块(前面的都是未使用的内存块),如果前面的内存块的next指针指向空了,则说明内存池中的内存块被使用完了。
下面示意了申请了3个内存块。
1.3 释放内存
从内存池使用的内存,如果使用结束了,则归还给内存池,继续给后续的申请者使用。
实际场景中,先申请的,不一定先释放,那内存池是如何回收之前分配出的内存块呢?
例如,依次申请出的3个内存块,低2个先使用结束了,要归还给内存池,这个时候,也是通过空闲指针freelist来管理,freelist会指向这个使用完毕的内存块,然后这个使用完毕的内存块的next指针,再指向刚才内存池中的最后一个空闲内存块。
使用链表的原因,就是因为释放的顺序和和申请的顺序不一样,这样就可以通过指针的方式,来管理空闲内存块的链接关系。即:一开始的链表指向,是按内存块的存储顺序依次指向的,后续是经历多次的内存申请和释放后,链表的指向关系,就与申请和释放的顺序有关了,通常情况就是指向的顺序是一种跳跃的指向,最终也是连成一个链表。
2 代码实践
下面通过代码来演示内存池的实现和实际的内存池的简单使用。
2.1 内存池类
定义一个MemPool内存池类,有两个成员变量:
- Block *freelist:记录空闲内存块的指针char *memory:记录内存池的地址
在构造函数中,会根据内存块的大小和数量,向系统申请内存,即内存池所需要的内存。
每个内存块,使用MemBlock这个数据结构,通过联合体的方式:
- struct MemBlock* next:记录下一个内存块的位置,在内存块空闲的使用char data:用户数据的地址,在内存块被使用时使用
class MemPool
{
private:
struct MemBlock
{
union
{
struct MemBlock* next;// 空闲链表指针(空闲时使用)
char data; // 用户数据起始地址(使用时使用)
};
};
MemBlock *freeList;
char *memory;
public:
MemPool(size_t blockSize, size_t maxCount)
{
PRINTF("blockSize:%zu, maxCount:%zun", blockSize, maxCount);
if (sizeof(MemBlock) > blockSize)
{
blockSize = sizeof(MemBlock);
PRINTF("need use sizeof(MemBlock):%zu as blockSizen", blockSize);
}
memory = newchar[blockSize * maxCount];
freeList = nullptr;
PRINTF("=============== memory addr:%p ==============n", memory);
for (size_t i = 0; i < maxCount; i++)
{
MemBlock *block = (MemBlock *)(memory + i * blockSize);
PRINTF("---------------> block[%zu] addr:%pn", i, (void *)block);
block->next = freeList;
freeList = block;
}
}
~MemPool()
{
delete[] memory;
}
void *Allocate()
{
if (!freeList) returnnullptr;
MemBlock *block = freeList;
freeList = block->next;
return block;
}
void Deallocate(void *ptr)
{
MemBlock *block = (MemBlock *)ptr;
block->next = freeList;
freeList = block;
}
void ShowFreeBlock()
{
if (!freeList)
{
PRINTF("no free blockn");
return;
}
MemBlock *tmpFreeList = freeList;
int i = 0;
while (tmpFreeList)
{
MemBlock *block = tmpFreeList;
tmpFreeList = block->next;
PRINTF("[%d]free block, addr:%pn", i++, block);
}
}
};
2.2 用户类
用于测试内存申请释放定义的类,可以根据自己的实际使用情况定义。
// 用户类
class User
{
public:
User(std::string name)
{
m_name = name;
PRINTF("%sn", m_name.c_str());
}
~User()
{
PRINTF("%sn", m_name.c_str());
}
void PrintInfo()
{
PRINTF("hello:%sn", m_name.c_str());
}
private:
std::string m_name;
};
2.3 用户类申请内存
继续封装一层,用于User类才内存池中申请内存并创建User类,参数:
- MemPool &pool:内存池std::string name:User类的参数
User *MemPoolNewUser(MemPool &pool, std::string name)
{
void* mem = pool.Allocate();
if (nullptr == mem)
{
PRINTF("no free block!!!n");
return nullptr;
}
// 定位new在已分配内存上构造User
User* p = new(mem) User(name);
return p;
}
2.4 完整测试代码
测试代码如下,先创建内存池,然后User类从内存池申请内存,最后释放内存,归还到内存池中。通过增加内存地址的打印,来观察内存池的内部情况以及验证内存池申请和释放后的内存块的链接指向情况。
// g++ test1.cpp -std=c++11 -o test1
#include <iostream>
#include <vector>
#include <string>
#include "PrintfDef.h"
class MemPool
{
private:
struct MemBlock
{
union
{
struct MemBlock* next;// 空闲链表指针(空闲时使用)
char data; // 用户数据起始地址(使用时使用)
};
};
MemBlock *freeList;
char *memory;
public:
MemPool(size_t blockSize, size_t maxCount)
{
PRINTF("blockSize:%zu, maxCount:%zun", blockSize, maxCount);
if (sizeof(MemBlock) > blockSize)
{
blockSize = sizeof(MemBlock);
PRINTF("need use sizeof(MemBlock):%zu as blockSizen", blockSize);
}
memory = newchar[blockSize * maxCount];
freeList = nullptr;
PRINTF("=============== memory addr:%p ==============n", memory);
for (size_t i = 0; i < maxCount; i++)
{
MemBlock *block = (MemBlock *)(memory + i * blockSize);
PRINTF("---------------> block[%zu] addr:%pn", i, (void *)block);
block->next = freeList;
freeList = block;
}
}
~MemPool()
{
delete[] memory;
}
void *Allocate()
{
if (!freeList) returnnullptr;
MemBlock *block = freeList;
freeList = block->next;
return block;
}
void Deallocate(void *ptr)
{
MemBlock *block = (MemBlock *)ptr;
block->next = freeList;
freeList = block;
}
void ShowFreeBlock()
{
if (!freeList)
{
PRINTF("no free blockn");
return;
}
MemBlock *tmpFreeList = freeList;
int i = 0;
while (tmpFreeList)
{
MemBlock *block = tmpFreeList;
tmpFreeList = block->next;
PRINTF("[%d]free block, addr:%pn", i++, block);
}
}
};
// 用户类
class User
{
public:
User(std::string name)
{
m_name = name;
PRINTF("%sn", m_name.c_str());
}
~User()
{
PRINTF("%sn", m_name.c_str());
}
void PrintInfo()
{
PRINTF("hello:%sn", m_name.c_str());
}
private:
std::string m_name;
};
User *MemPoolNewUser(MemPool &pool, std::string name)
{
void* mem = pool.Allocate();
if (nullptr == mem)
{
PRINTF("no free block!!!n");
returnnullptr;
}
// 定位new在已分配内存上构造User
User* p = new(mem) User(name);
return p;
}
// 测试内存池
void TestMemPool()
{
// 初始化内存池
MemPool pool(sizeof(User), 5);
User *Obj1 = MemPoolNewUser(pool, "obj1");
User *Obj2 = MemPoolNewUser(pool, "obj2");
User *Obj3 = MemPoolNewUser(pool, "obj3");
PRINTF("addr, Obj1:%p, Obj2:%p, Obj3:%pn", Obj1, Obj2, Obj3);
pool.ShowFreeBlock();
// 先使用和释放Obj2
Obj2->PrintInfo();
Obj2->~User(); // 调用析构函数释放成员资源
pool.Deallocate(Obj2); // 内存放回空闲链表
pool.ShowFreeBlock();
// 然后使用和释放Obj3
Obj3->PrintInfo();
Obj3->~User(); // 调用析构函数释放成员资源
pool.Deallocate(Obj3); // 内存放回空闲链表
pool.ShowFreeBlock();
// 最后使用和释放Obj1
Obj1->PrintInfo();
Obj1->~User(); // 调用析构函数释放成员资源
pool.Deallocate(Obj1); // 内存放回空闲链表
pool.ShowFreeBlock();
}
int main()
{
TestMemPool();
return0;
}
2.5 运行结果
运行结果如下:
3 总结
本篇通过一个最基础的内存池的示例,介绍了内存池的基本原理。实际的使用场景中,需要继续完善这个内存池,比如预先分配多批不同大小的内存块,便于不同大小的对象的内存申请,对于多线程场景,增加锁机制,确保申请和释放时的线程安全等。
432