定长内存池的原理是预先向操作系统申请一大段堆区空间并进行管理,在接下来的程序中,如果需要使用堆区空间,则就从先前开辟的空间中分,而不再直接向操作系统申请,以减少程序调用操作系统接口的次数,提高运行效率。
定长内存池有多种实现方式,这里采用T来对内存进行分块,T时用户传给内存池模板的模板参数,由memory指针管理内存池从操作系统处直接申请来的空间,称为初始资源指针;由freelist指针管理用户还回来的内存块,称为回收队列指针。
简要过程如图所示:
值得注意的是,既然是实现内存池这种轮子功能,自然内存池本身不宜再使用new和malloc来向操作系统申请空间,在windows系统中可采用VirtualAlloc系统接口,Linux系统中可采用sbrk和mmap系统接口。
调用系统接口申请空间的函数:
#include <iostream>
#if _WIN32
#include <Windows.h>
#else
#include <unistd.h>
#include <sys/mman.h>
#endif // _WIN32
using std::cout;
using std::endl;
using std::bad_alloc;
inline static void* systemalloc(size_t kpage)
{
void* ptr = nullptr;
#ifdef _WIN32
ptr = VirtualAlloc(NULL, kpage * 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
if (kpage <= 32)//申请空间小于32页,采用sbrk接口直接扩大堆区尾指针,并将扩大的空间指针返回
{
ptr = sbrk(kpage * 4096);
}
else
{
ptr= mmap(NULL, kpage*4096, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);//申请空间大于32页,则采用mmap接口,由操作系统在空闲堆区中选择一块合适的空间返回
}
#endif // _WIN32
if (ptr == NULL)
{
throw bad_alloc();
}
else
return ptr;
}
另外,不要忘了使用定位new和手动调用析构函数来手动初始化和清理对象资源,因为在从内存池中申请来的空间定义对象,是不会调用构造函数和析构函数的(使用new运算符会自动调用,是因为new封装了调用对象构造函数和析构函数的功能)。
程序主体:
template <class T>
class ObjectPool
{
public:
ObjectPool():Tsize(sizeof(T))
{}
T* New()
{
T* ptr = nullptr;
if (freelist)
{
ptr = (T*)freelist;
freelist = *(void**)freelist;
}
else
{
if (leftbytes < Tsize)
{
memory = (char*)systemalloc(Number_pages);//一次开辟Number_pages页,默认为10页,1页4096B
leftbytes = Number_pages * 4096;
}
ptr =(T*) memory;
size_t objectsize = Tsize >= sizeof(void*) ? Tsize : sizeof(void*);
memory += objectsize;
leftbytes -= objectsize;
}
new(ptr)T;//定位new调用T的默认构造函数
return ptr;
}
void Delete(T*obj)
{
obj->~T();//调用析构函数
*(void**)obj = freelist;
freelist =(void*) obj;
}
private:
char* memory = nullptr;
int leftbytes = 0;
void* freelist = nullptr;
size_t Tsize;
};
通关TestObjectPool比较直接采用new运算符和使用定长内存池申请空间的速度: