引子
- new实际分配的内存:
- 从new实际分配的内存可以看出,除了我们实际需要的内存之外,系统分配了很多增加的内容,例如开头和结尾的两个cookie,各8byte。这两个cookie记录了实际分配空间的大小,同时在delete的时候告诉系统实际需要释放的空间。
- 为了减少大量小空间被分配时,过多cookie造成的内存浪费,我们引入了内存池。内存池的思想是一次性申请一大块空间,每次使用时返回一小块,这样做的优点在于:既减少了申请内存操作的次数,提升了程序运行的效率;又减少了cookie带来的内存浪费。
- 通常内存池内部是一个单向链表将内存片串联起来,便于管理;
一、单独类内存池
class A
{
public:
A(int x) :i(x) {};
int get() { return i; }
void* operator new(size_t);
void operator delete(void*, size_t);
private:
A* next;
static A* freeStore; //指向可用首地址
static const int Achunk; //内存池可容纳的对象个数
private:
int i;
};
A* A::freeStore = 0;
const int A::Achunk = 24;
void* A::operator new(size_t size)
{
A* p;
if (!freeStore)
{
//linked list is empty
size_t chunk = Achunk * size;
freeStore = p = reinterpret_cast<A*>(new char[chunk]);
//将一大快分片
for (; p != &freeStore[Achunk - 1]; ++p)
{
p->next = p + 1;
}
p->next = 0;
}
p = freeStore;
freeStore = freeStore->next;
return p;
}
void A::operator delete(void* p, size_t)
{
// 将delete object插回free list前端
(static_cast<A*>(p))->next = freeStore;
freeStore = static_cast<A*>(p);
}
- 内存池的优势在于提高运行速度,减少内存浪费;
- 这样处理存在的问题时是:是否有必要为了消除cookie而额外引入一个4byte的指针,对于上例来说,膨胀率等于100%,以下例子巧妙解决了这个问题;
二、改进版单独类内存池
class A
{
private:
struct ARep
{
int i;
char type;
};
private:
union
{
ARep rep;
A* next;
};
public:
A(int x) :i(x) {};
int get() { return i; }
void* operator new(size_t);
void operator delete(void*, size_t);
private:
static A* freeStore; //指向可用内存首地址
static const int Achunk; //内存池可容纳的对象个数
private:
int i;
};
A* A::freeStore = 0;
const int A::Achunk = 24;
void* A::operator new(size_t size)
{
if (size != sizeof(A))
return ::operator new(size);
A* p = freeStore;
if (p)
freeStore = p->next;
else
{
//linked list is empty
size_t chunk = Achunk * size;
A* newBlock = static_cast<A*>(::operator new(Achunk * sizeof(A)));
//将一大快分片,并串联
for (int i = 1; i < Achunk; ++i)
{
newBlock[i].next = &newBlock[i + 1];
}
newBlock[Achunk - 1].next = 0; //结束list
p = newBlock;
freeStore = &newBlock[1];
}
return p;
}
void A::operator delete(void* p, size_t)
{
// 将delete object插回free list前端
(static_cast<A*>(p))->next = freeStore;
freeStore = static_cast<A*>(p);
}
- 使用嵌入式指针后,我们借用A对象所占空间的前4字节,用于连接空闲内存块(储存指针),一旦这一块被分配出去,这4个字节不再需要;可以说嵌入式指针和成员变量是在不同时期使用了同一块内存;
- 这两种内存池的实现方式类似,都是为单独的类进行内存管理,这失去了重用性,我们如何解决这一问题呢?
三、静态分配器
class m_allocator
{
private:
struct obj
{
struct obj* next; //embedded pointer
};
public:
void* allocate(size_t);
void deallocate(void*, size_t);
private:
obj* freeStore = nullptr;
const int CHUNK = 5;
};
void* m_allocator::allocate(size_t size)
{
obj* p;
if (!freeStore)
{
size_t chunk = CHUNK * size;
freeStore = p = (obj*)malloc(chunk);
for (int i = 0; i < CHUNK - 1; ++i)
{
p->next = (obj*)((char*)p + size);
p = p->next;
}
p->next = nullptr;
}
p = freeStore;
freeStore = freeStore->next;
return p;
}
void m_allocator::deallocate(void* p, size_t)
{
((obj*)p)->next = freeStore;
freeStore = (obj*)p;
}
- 这样一来,app classes 不再与内存分配的细节纠葛,而全权交给allocator去管理;