接下来我们定义一个单模的类,用来管理内存的申请、释放、重用等
class MemPool
{
private:
MemPool();
bool AllocPage(); // 分配一个页
void* AllocLarge(int nSize, const char* const szFileName, int nLine, bool bIsArray); // 分配一个大内存(>2048)
bool IsInBlockPool(Prefix* pPrefix) const { return (pPrefix->d.pPostfix->nBlockPoolIndex >= 0); }; // 判断该内存块是否在某个尺寸的链表中等待被重用
public:
~MemPool();
static MemPool& GetInstance(); // 单模实例
void* Alloc(int nSize, const char* const szFileName, int nLine, bool bIsArray); // 分配内存的入口函数
void Free(void* pMem); // 释放内存
void AddCleanup(CleanupHandler pHandler, void* pParam); // 添加某个清理函数,后面会举例介绍它的用处。
private:
MemPageHeader* m_pFirstPage; // 指向池中第一个页
Prefix* m_pLarge; // 指向大内存块链表
Prefix* m_ppBlockPools[20]; // 各种尺寸内存块的链表池。最多20种不同尺寸
int m_nBlocksAllocated[20]; // 记录每种尺寸的内存块的已分配个数
struct Cleanup {
CleanupHandler pHandler;
void *pParam;
Cleanup* pNext;
} *m_pCleanups; // 清理函数的链表
static MemPool m_heap; // 单模实例
CriticalSection m_cs; // 用来保证线程安全。这是个自定义的类,封装了EnterCriticalSection等函数,忽略不影响阅读
};
其中涉及到的清理函数的定义如下。暂时可以不用管它,以后会介绍。
typedef void (*CleanupHandler)(void *pParam);
下面我们来看具体的实现。首先是这个单模类的构造函数:
MemPool::MemPool()
{
m_pFirstPage = NULL; // 起始时没有从系统申请任何页
m_pLarge = new Prefix; // 为大内存块的链表生成一个头,这个头元素只是用来简化链表的元素删除操作,没有其他意义
memset(m_pLarge, 0, sizeof(Prefix));
m_pCleanups = NULL; // 起始时没有注册任何清理函数(清理函数的作用后面再讲)
for (int i = 0; i < 20; i ++)
{
m_ppBlockPools[i] = new Prefix; // 为各尺寸的内存池链表生成一个头,这个头元素只是用来简化链表的元素删除操作,没有其他意义
memset(m_ppBlockPools[i], 0, sizeof(Prefix));
}
memset(m_nBlocksAllocated, 0, 20 * sizeof(int));
}
和析构函数
MemPool::~MemPool()
{
// 调用所有清理函数做清理。具体左右后面举例讲。
for (Cleanup *p = m_pCleanups; p != NULL; p = p->pNext)
{
if (p->pHandler)
p->pHandler(p->pParam);
}
// 释放所有清理函数相关结构
while (m_pCleanups != NULL)
{
Cleanup *p = m_pCleanups;
m_pCleanups = m_pCleanups->pNext;
delete p;
}
// 释放所有页
while (m_pFirstPage)
{
MemPageHeader *p = m_pFirstPage;
m_pFirstPage = m_pFirstPage->d.pNext;
_aligned_free(p);
}
// 释放所有大内存(>2048字节)块。
Prefix *p = m_pLarge;
m_pLarge = m_pLarge->d.pNext;
delete p; // 释放链表头
while (m_pLarge != NULL) // 如果软件里无内存泄露,应为NULL不会进入循环
{
p = m_pLarge;
m_pLarge = m_pLarge->d.pNext;
_aligned_free(p);
}
// 释放各尺寸的链表头
for (int i = 0; i < 20; i ++)
delete m_ppBlockPools[i];
}
单模类的实例获取,这个不用介绍了吧
MemPool& MemPool::GetInstance()
{
return m_heap;
}
下面是我们的重头戏,内存分配的入口函数
void* MemPool::Alloc(int nSize, const char* const szFileName, int nLine, bool bIsArray)
{
if (nSize < MAX_ALLOC_FROM_PAGE) // <2048
{
int nMinPower2 = 16; // 至少分配16字节
int i = 0;
for (; nMinPower2 < nSize; nMinPower2 = nMinPower2 << 1, i ++); // 找到不小于申请尺寸的最小的2的幂,即规定的16、32、64等尺寸
SYNCHRONIZATION(); // 线程安全宏,用到了自定义的CriticalSection,不用管
// 查找对应尺寸的池中是否有内存块
if (m_ppBlockPools[i]->d.pNext) // 链表头下面有元素
{
Prefix *pPrefix = m_ppBlockPools[i]->d.pNext;
// 从链表中分离该内存块
m_ppBlockPools[i]->d.pNext = pPrefix->d.pNext;
if (pPrefix->d.pNext)
pPrefix->d.pNext->d.pPrev = m_ppBlockPools[i];
pPrefix->d.pPrev = pPrefix->d.pNext = NULL;
pPrefix->d.pPostfix->nBlockPoolIndex = -1; // 负数代表这个内存块不在池中了,而是被使用了
pPrefix->d.pPostfix->nActualSize = nSize; // 记录申请的真实尺寸
m_nBlocksAllocated[i] ++; // 计数分配的该尺寸内存块
return pPrefix->pMem; // 返回真正可使用的指针地址(去掉前缀)
}
// 从页中分配
const int nAllocSize = nMinPower2;
do {
for (MemPageHeader *pPage = m_pFirstPage; pPage != NULL; pPage = pPage->d.pNext) // 遍历每个页。这个可能会很慢,但有办法,以后再说
{
if (pPage->d.pEnd >= pPage->d.pLast + sizeof(Prefix) + nAllocSize + sizeof(Postfix)) // 页里有足够空间
{
Prefix *pPrefix = (Prefix*) pPage->d.pLast;
pPrefix->d.pPostfix = (Postfix*)(pPrefix->pMem + nAllocSize);
pPrefix->d.pPostfix->pPrefix = pPrefix;
pPrefix->d.pPostfix->nBlockPoolIndex = -1; // 负数代表这个内存块不在池中了,而是被使用了
pPrefix->d.pPostfix->nActualSize = nSize; // 记录申请的真实尺寸
pPrefix->d.pPrev = pPrefix->d.pNext = NULL;
pPrefix->d.pPage = pPage;
pPage->d.pLast += sizeof(Prefix) + nAllocSize + sizeof(Postfix); // 移动页尾巴(页里空余内存的起始指针)
m_nBlocksAllocated[i] ++; // 计数分配的该尺寸内存块
return pPrefix->pMem; // 返回真正可使用的指针地址(去掉前缀)
}
}
} while (AllocPage()); // 所有页都没有足够内存,现在我们需要分配一个新的页
std::cerr << "Memory allocation failure" << std::endl;
return NULL;
}
return AllocLarge(nSize, szFileName, nLine, bIsArray); // 分配一块大内存
}
休息一下吧,下一章再说