一、为什么需要内存池?
按照标准库的写法,new一个对象的时候,会malloc一块内存;delete的时候会free这块内存。频繁的malloc与free存在两个问题:
(1)耗时,这两个都是操作系统层级的函数,会相对耗时。
(2)malloc的内存中存在上下两个cookie,用来标记这块内存的起止地址,而且内存要向上取8的倍数。如果大量malloc,那么内存内存浪费还是比较惊人的。
这个问题,内存池可以很好地解决。
二、什么是内存池?
既然频繁的malloc不是一件好事情,那么我们可以一次malloc申请一大块内存。当需要new一个对象的时候,可以在已经申请好的内存里面执行构造函数。在我们delete这个对象之后,这个内存上还可以用来构造其它对象。
这就是内存池设计的基本思想:一次性申请大块内存,用于多次new对象,避免频繁的malloc与delete。
三、链表实现内存池的基本思想
你如果在中文搜索引擎上面搜索“内存池的实现”,出现最多的方法就是链表实现内存池。有一说一,这个名字取得不好,非常容易让人产生误解。比如:
(1)用链表实现内存池,链表本来就是不连续的内存,那就说明要malloc多次,那么这个内存池有什么意义???
答案:内存池就是一次(扩容情况下多次)malloc的内存,是连续的内存。只不过将连续的内存使用链表管理起来了。
(2)既然有了一个连续的内存,为什么要将其用链表分割呢?
答案:方便我们即使获取被释放的内存,将其纳入可用内存。举个例子,假如我们的内存池可以容纳若干个对象,我们依次按照地址顺序构造了三个对象,这个时候如果释放了第一个对象,可以把这块地址通过指针管理起来。一句话:内存池在构造的时候是连续地址,但是在多次获取和释放以后,就会变成局部离散的地址!
内存池的基本设计思想:
(1)一次申请大块内存,将其按照对象所需的大小切片。各个内存片之间通过指针串起来。内存池构造好的时候,这个链表的各个结构体内存地址是顺序的。
(2)如果需要使用内存,将链表头指向下一个节点,原来的链表头返回。这个链表头中有足够的空间供给所需对象的构造。
(3)如果需要释放内存,则将这块内存原封不动的插到链表头,到这里就可以看出来,多次申请和释放内存后,链表就不是有序的了。
(4)最关键一步:使用这个内存池的类,要重载operator new和operator delete函数,使用内存池版本。
看代码:
#include <iostream>
using namespace std;
template<int ObjectSize, int maxSize = 20>
class MemPool
{
struct Node{ //实际用于构造对象的内存
Node * next = nullptr;
char data[ObjectSize]; //足够大的空间容纳对象
};
private:
Node * freeNodeHead{nullptr};
public:
MemPool(){
cout<<sizeof(Node)<<endl;
freeNodeHead = new Node[maxSize];
for(int i=0;i<maxSize-1;i++){
freeNodeHead[i].next=&freeNodeHead[i+1];
}
}
~MemPool(){
delete [] freeNodeHead;
}
void * malloc(){
if(freeNodeHead == nullptr){
freeNodeHead = new Node[maxSize];
for(int i=0;i<maxSize-1;i++){
freeNodeHead[i].next=&freeNodeHead[i+1];
}
}
cout<< "malloc a mem" <<endl;
Node * ret = freeNodeHead;
freeNodeHead = freeNodeHead->next;
ret->next = nullptr;
return ret;
};
void free(void * p){
cout<<"free a mem"<<endl;
Node * newNode = (Node * )p;
newNode->next = freeNodeHead;
freeNodeHead = newNode;
}
};
class Test {
int No{1};
public:
void print() {
cout << this << ": "<<endl;
}
void* operator new(size_t size);
void operator delete(void* p);
};
MemPool<sizeof(Test), 10> mp;
void* Test::operator new(size_t size) {
return mp.malloc();
}
void Test::operator delete(void* p) {
mp.free(p);
}
int main()
{
Test * p1 = new Test;
p1->print();
Test * p2 = new Test;
p2->print();
delete p1;
Test * p3 = new Test;
p3->print();
delete p2;
delete p3;
}
猜测一下,如果p1和p3的地址一模一样,就说明我们的内存池成功了。
malloc a mem
0x19c53785f20:
malloc a mem
0x19c53785f30:
free a mem
malloc a mem
0x19c53785f20:
free a mem
free a mem
一模一样,成功了。
但是好像有点问题,地址偏移量什么是16个字节?因为内存对齐机制。虽然ObjectSize+指针大小小于16,但是会对齐为16个字节。