根据C++STL中的空间配置器,实现一个轻量级的内存池,由于空间配置器虽然解决了外部碎片的问题,提高了效率,但它的缺陷在于若使用二级空间配置器,它不会主动释放已经空闲的内存块,还给操作系统,而是将自己申请的内存块全部挂在自由链表上,自己不用,别的进程也不可以用,造成极大的内存空间的浪费,很可能导致很多别的进程无内存可用的情况。而轻量级的内存池不仅解决外部碎片问题,并且解决释放内存问题,它使用多少用多少,即在堆上申请的内存基本都在使用,不会大量闲置,并且当所有内存块都已不在使用时,在直接释放还给操作系统。
实现结构如下:
在以上结构实现中,首先要考虑以下几点:
1、链表节点memory指向的内存块大小以2倍增长(即第一个节点若挂3块内存块,第二个节点则挂6块);
2、在实现中,若第一个节点中挂的内存块有空闲,则不会开辟链表第二个节点并相应的开辟新内存,是直接使用前一个节点空闲的内存块,以致不会造成一个程序对内存的大量占用却不使用的情况,所以由此也可得出若要开辟一个新节点,说明前面节点管理的内存都已被使用;
3、在不断开辟申请使用内存过程中,前面也必定有使用完成之后已释放的内存块,所以在申请内存时先要检测先前是否有释放的内存块,若有,则使用已释放的内存块。
代码实现:
#pragma once
using namespace std;
#include <string>
template <class T>
class ObjectPool
{
struct Node
{
void* memory; //指向挂着的内存块
size_t n; //挂的内存块个数
Node* next; //指向下一个内存块节点头
Node(size_t nobjs)
:n(nobjs)
,next(NULL)
{
memory=::operator new(n*GetSize());
}
~Node()
{
::operator delete(memory);
memory=NULL;
n=0;
next=NULL;
}
};
public:
ObjectPool(size_t nobjs=16,size_t maxNobjs=1024)
:_initNobjs(nobjs)
,_maxNobjs(maxNobjs)
,_useInCount(0)
,_lastDelete(NULL)
{
_head=new Node(_initNobjs);
_tail=_head;
}
~ObjectPool()
{
Node* cur=_head;
while(cur)
{
Node* del=cur;
cur=cur->next;
delete del;
del=NULL;
}
_head=_tail=NULL;
_lastDelete=NULL;
_initNobjs=_maxNobjs=_useInCount=0;
}
public:
//封装一层接口
template <class Val>
T* New(const Val& val)
{
void* obj=Allocate();
return new(obj)T(val);//new定位表达式初始化
}
void Delete(T* ptr)
{
if(ptr)
{
ptr->~T();//若为自定义类型,先调用其类型的析构函数
Deallocate(ptr); //释放该内存
}
}
protected:
void* Allocate() //申请资源
{
//1.先查找内存是否有释放的内存块
if(_lastDelete)
{
void* ptr=_lastDelete;
_lastDelete=*((T**)_lastDelete);
return ptr;
}
//2.没有释放开辟新内存
if(_useInCount>=_tail->n)
AllocNewNode();
//返回内存块
void* ptr=(char*)(_tail->memory)+_useInCount*GetSize();
++_useInCount;
return ptr;
}
void Deallocate(void* ptr)
{
//链表的隐形头插
*((T**)ptr)=_lastDelete; //取当前释放的内存块内容的前(T*)个字节,存放上一次释放的内存块的地址
_lastDelete=(T*)ptr; //标记释放的内存块的位置
}
void AllocNewNode() //申请新节点
{
size_t n=_tail->n*2;//开辟内存块数为上一次2倍
if(n>=_maxNobjs)
n=_maxNobjs;
Node* node=new Node(n);//链表尾插
_tail->next=node;
_tail=node;
_useInCount=0;//更新
}
inline static size_t GetSize()//32、64位系统兼容,指针大小分别为4、8字节,使一个内存块至少存放下一个指针大小
{
return sizeof(T)>sizeof(T*)?sizeof(T):sizeof(T*);
}
protected:
size_t _initNobjs; //初始化开辟内存块个数
size_t _maxNobjs; //开辟内存块个数最大值
Node* _head; //管理内存块链表的头
Node* _tail; //管理内存块链表的尾
size_t _useInCount; //已用的内存块个数
T* _lastDelete; //当前所有释放的内存块的链表头
};
void Test()
{
ObjectPool<int> obj(3,1024);
int* p1;int* p2;int* p3;int* p4;int* p5;
p1=obj.New(1);
p2=obj.New(2);
p3=obj.New(3);
p4=obj.New(4);
obj.Delete(p2);
p5=obj.New(5);
obj.Delete(p4);
obj.Delete(p3);
obj.Delete(p1);
int* p6=obj.New(6);
ObjectPool<string> pool1;
string* p7 = pool1.New("测试");
pool1.Delete(p7);
}
在以上代码具体实现中,
1.在构造链表节点时,即构造了节点,又同时开辟了指定大小的新内存,初始化了指针memory,挂起了内存块。
2.在对使用完的内存块释放调用Deallocate()函数时,运用了链表的隐式头插,如图:
即在这次释放的内存块中的sizeof(T*)个字节存放上一次释放的内存块首地址,并更新_lastDelete,使它指向释放内存块的链表头,以此也实现了一物二用的效果。所以在申请资源调用Allocate()函数时,检查是否有释放的空闲块时,若有则实现隐形头删,返回_lastDelete现指向的内存地址,并更新它的值使它指向当前内存块下一个。
3.为实现第2点提出的一物二用,即每个内存块大小则至少要满足存储sizeof(T*)大小(一个指针大小),为了满足系统的兼容性,则必须保证每次申请的内存块大小>=sizeof(T*),若小于,则自动提升为sizeof(T*)。