内存池的实现

根据C++STL中的空间配置器,实现一个轻量级的内存池,由于空间配置器虽然解决了外部碎片的问题,提高了效率,但它的缺陷在于若使用二级空间配置器,它不会主动释放已经空闲的内存块,还给操作系统,而是将自己申请的内存块全部挂在自由链表上,自己不用,别的进程也不可以用,造成极大的内存空间的浪费,很可能导致很多别的进程无内存可用的情况。而轻量级的内存池不仅解决外部碎片问题,并且解决释放内存问题,它使用多少用多少,即在堆上申请的内存基本都在使用,不会大量闲置,并且当所有内存块都已不在使用时,在直接释放还给操作系统。

实现结构如下:

内存池的实现_第1张图片

在以上结构实现中,首先要考虑以下几点:
1、链表节点memory指向的内存块大小以2倍增长(即第一个节点若挂3块内存块,第二个节点则挂6块);
2、在实现中,若第一个节点中挂的内存块有空闲,则不会开辟链表第二个节点并相应的开辟新内存,是直接使用前一个节点空闲的内存块,以致不会造成一个程序对内存的大量占用却不使用的情况,所以由此也可得出若要开辟一个新节点,说明前面节点管理的内存都已被使用;
3、在不断开辟申请使用内存过程中,前面也必定有使用完成之后已释放的内存块,所以在申请内存时先要检测先前是否有释放的内存块,若有,则使用已释放的内存块。
代码实现:

#pragma once
using namespace std;
#include 

template 
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 
	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 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 pool1;
	string* p7 = pool1.New("测试");
	pool1.Delete(p7);
}
在以上代码具体实现中,

1.在构造链表节点时,即构造了节点,又同时开辟了指定大小的新内存,初始化了指针memory,挂起了内存块。
2.在对使用完的内存块释放调用Deallocate()函数时,运用了链表的隐式头插,如图:

内存池的实现_第2张图片

即在这次释放的内存块中的sizeof(T*)个字节存放上一次释放的内存块首地址,并更新_lastDelete,使它指向释放内存块的链表头,以此也实现了一物二用的效果。所以在申请资源调用Allocate()函数时,检查是否有释放的空闲块时,若有则实现隐形头删,返回_lastDelete现指向的内存地址,并更新它的值使它指向当前内存块下一个。
3.为实现第2点提出的一物二用,即每个内存块大小则至少要满足存储sizeof(T*)大小(一个指针大小),为了满足系统的兼容性,则必须保证每次申请的内存块大小>=sizeof(T*),若小于,则自动提升为sizeof(T*)。

你可能感兴趣的:(c++学习,Linux,C++学习笔记,数据结构)