101 C++内存高级话题 内存池概念,代码实现和详细分析
零 为什么要用内存池?
从前面的知识我们知道,当new 或者 malloc 的时候,假设您想要malloc 10个字节,
char * pchar = new char[10];
char *pchar1 = malloc(10);
实际上编译器为了 记录和管理这些数据,做了不少事情,类似这张图。
从上述看到,每new 一个 class都会使用这些字节:
4+(30到60)+(真的分配的10字节)(10到几十个)+4
也就是说:为了这10个字节,实际上背后服务的有更多的字节。
如果在某一个场景下,我们需要new 出来大量的class,例如卡牌的10连抽,100连抽,那么每次new一个class,都会有大量的背后服务的字节使用。有没有一种方法可以减少这种背后服务的字节数量呢?
因此C++的前辈们就搞了一个内存池。
一 内存池的概念和实现原理概述
概念:当malloc 或者 new 的时候,为了节约内存,创建了内存池的方法。当阅读完下面的原理后,还会知道会节省malloc或者new 的次数,因此也会提高效率,但是提高的不多。(提高不多的原因有两点:1,本身malloc 和new 执行效率就很高,2,因此内存池的内部实现需要使用链表将创建的真正使用的内存串联起来,这个串的过程也要花费时间)
我们假设之前class 是占用8个字节,new一次会出来对我们有用的8个字节的class,
4+(30到60)+(真的分配的8字节)(10到几十个)+4
但是有更多的字节是为这8个字节服务,现在通过重写 operator new 的方法,让一次new 出来80个字节
4+(30到60)+(真的分配的80字节)(10到几十个)+4
这就存在着另一个问题,我们每次实际上是需要8个字节的,因此需要将这80个字节,怎么串起来,让每次都拿8个字节,
还存在着另一个问题,就是这8个字节的回收,因此 还需要 重写 operator delete的方法实现。
注意的点:一般创建内存时,会创建一个class 的整倍数。
使用链表将其串联起来。
operator delete的时候,实际上是让串联的指针重新指,而不是真的 free 或者delete。
因此理论上:内存池始终都会拿着申请的内存。不会真正的释放。
二 针对一个类的内存池实现演示代码
//内存池代码演示,Teacher36我们看做是一个卡牌类,这个卡牌类,肯定有如下的类型:红,橙,绿,蓝四种类型分别代表不同的级别。肯定还有名字,技能1,技能2,技能3,技能4,属性(金木水火土)。
//假设我们这个 卡牌每天都会给用户免费抽3次,vip用户每天抽10次,
//那就意味着,服务器每天要处理大量的抽卡行为,这就能用到 内存池技术了。
class Teacher36 {
public:
int jibie; //红 ,橙,绿,蓝
string name; // 名字,
int jineng1; //技能1
int jineng2;//技能2
int jineng3;//技能3
int jineng4;//技能4
int shuxing;// 属性 金木水火土
//我们还需要自己做一下统计, new Teacher 一次,统计一次。
static int m_iCout;
static int m_iMallocCount;//malloc一次,统计一次,每次malloc,会分配10个 * Teacher36的大小。
private:
Teacher36 *next;//作用是 将 new 出来的字节挨个 链接起来
static Teacher36* m_FreePosi;//总是指向一块可以分配出去的内存的首地址
static int m_sTrunkCount;// 一次分配多少倍的该类内存
public:
//第1步.重写operator new 函数
static void *operator new(size_t size) {
//1.0 之前的写法
//Teacher36 *pTeacher36 = (Teacher36*)malloc(sizeof(Teacher36));
//return pTeacher36;
//1.1 现在的写法,不能只malloc 一个 Teacher36的size,要malloc一堆
//假设一次弄10个Teacher36的大小,return那个出去呢?
//很显然,总是要弄一个 Teacher36 * 返回出去。
//很显然,弄一个出去,剩余的9个怎么弄呢?
//这就存在将malloc的这10个Teacher 管理起来的逻辑,这里需要一个链表,将剩余的9个链接起来,我们通过 Teacher36 * next完成
//然后让最后一个Teacher36 * 指向 nullptr
Teacher36* templink;//让这个templink始终指向可以返回出去的Teacher36 *
if (m_FreePosi==nullptr) {
//当m_FreePosi为null的时候,代表一定要申请内存,且要申请一大块你内存
//注意:从上一节的知识我们知道 ,这里参数 size就是一个 Teacher36的大小。
//cout << "size = " << size << " m_sTrunkCount = " << m_sTrunkCount << endl;
size_t realSize = m_sTrunkCount * size;
//创建 realSize大小的空间,并且将这个空间强转成
m_FreePosi = reinterpret_cast<Teacher36 *>(new char[realSize]);
templink = m_FreePosi;
//把分配出来的这一大块内存(10小块)彼此要链接起来,方便后来使用,templink指向的是
for (; templink!= &m_FreePosi[m_sTrunkCount - 1];++templink) {
templink->next = templink + 1;
}
//让最后一个链 的next指向null
templink->next = nullptr;
++m_iMallocCount;//统计一下malloc的次数
}
//当m_FreePosi存在空间的时候,就把m_FreePosi给 templink,然后将templink返回出去
templink = m_FreePosi;
//既然当前的 m_FreePosi的一小块被返回出去了,那么下一次,就要返回m_FreePosi的next,因此这里还需要将m_FreePosi = m_FreePosi->next;
m_FreePosi = m_FreePosi->next;
//然后我们再记录一下, new 了多少次Teacher36
++m_iCout;
return templink;
}
//第2步.重写 operator delete函数,注意和之前的不同,这里不是直接销毁这块
static void operator delete(void *phead) {
//2.0之前的写法,是要真的free掉这块内存
//free(phead);
//return;
//2.1 让被释放的phead的next指向 m_FreePosi,然后让phead变成m_FreePosi
static_cast<Teacher36*>(phead)->next = m_FreePosi;
m_FreePosi = static_cast<Teacher36 *>(phead);
}
};
int Teacher36::m_iCout = 0;//new 的次数
int Teacher36::m_iMallocCount = 0;//malloc的次数
Teacher36* Teacher36::m_FreePosi = nullptr;//第一次肯定是没有数据的,也就是指向nullptr
int Teacher36::m_sTrunkCount = 10;//一次分配多少倍的空间
void main() {
cout << sizeof(int) << endl; //4
cout << sizeof(int*) << endl; //4
cout << sizeof(long) << endl; //4
cout << sizeof(long*) << endl; //4
cout << sizeof(string) << endl;//28
cout << "sizeof(Teacher36) = " << sizeof(Teacher36)<< endl;//56
for (int i = 0; i < 1000;i++) {
Teacher36 *ptea = new Teacher36;
}
cout << "申请分配内存的次数为:" << Teacher36::m_iCout <<endl;
cout << "实际malloc内存的次数为:" << Teacher36::m_iMallocCount << endl;
}
operator new 的代码说明
我们已每次申请 5个单位的Teacher36说明
当5个都用完的时候,再来new Teacher,就继续申请一块内存
operator delete 的代码说明:
假设申请了2次 malloc了,且已经new 了9次代码了
//2.1 让被释放的phead的next指向 m_FreePosi,然后让phead变成m_FreePosi
static_cast<Teacher36*>(phead)->next = m_FreePosi;
m_FreePosi = static_cast<Teacher36 *>(phead);
三,内存池代码后续说明。
我们代码改动一下,每次分配的内存为5个teacher36的大小,new 20次,观察log会发现,每5个之间都是56个字节。
注意的是:在上述的代码,实际上我们并没有释放内存。一直在持有这些内存,因此使用内存池要注意这一点。
四。嵌入式指针。embedded pointer
从上面的代码中可以看出,实际上需要一个指针,Teacher36 * next。这4个字节是为了将管理分配出来的内存而写的,实际上是可以不需要的。
那么我们这里就要引入一个 嵌入式指针的概念。
实际上,在内存池的代码中,通常结合 嵌入式 指针来工作。
相关原理:
借用Teacher36所占用空间的前4个字节,当做指针,链接管理后续分配的内存。这样 就可以省下来这个next指针。
从上述原理,我们也可以看出,如果要使用嵌入式指针,class 类的大小需要大于等于4个字节。
也就是不能是空类,空类只占用1个字节。
代码实现;
//嵌入式指针.
//struct obj 放在类外边和放在类里面是一样的。,但是一般都会放在类里面,因此称为嵌入式指针。
class Teacher37 {
public:
int m_i;
int m_j;
public:
struct obj {
struct obj *next;//这个next就是个嵌入式指针。
//自己是一个obj结构对象,
//那么把自己这个对象的next指针指向另外一个obj结构对象,
//最终,把多个自己这种类型的对象通过链串起来
};
};
void main() {
cout << sizeof(Teacher37) << endl;//8 ,两个int 各占4个字节。struct obj是类型声明,是不占用空间的。
}
五。利用嵌入式指针改动内存池代码。
这里要解决两个问题:
1.单独的为内存池技术来写一个类,不是在单独的Teacher36上写,也不是为单独的Teacher37写。而是单独的写一个通用的类
2.使用嵌入式指针改动代码。
//专门的内存池类
class myallocator { //必须保证应用本类的类的sizeof 不少于4个字节,否则会报错
public:
//分配内存接口
void *allocate(size_t size) {
obj *tmplink;
if (m_FreePosi == nullptr) {
//为空,我要申请内存,要申请一大块内存
size_t realsize = m_sTrunkCout * size;//申请m_sTrunkCout 这么多倍的内存
m_FreePosi = (obj *)malloc(realsize);
tmplink = m_FreePosi;
//把分配出来的这一大块内存(5小块),彼此链接起来,供后续使用
for (int i = 0; i < m_sTrunkCout - 1;++i) {
tmplink->next = (obj *)((char *)tmplink + size);
tmplink = tmplink->next;
}//end for
tmplink->next = nullptr;
}
tmplink = m_FreePosi;
m_FreePosi = m_FreePosi->next;
return tmplink;
}
void deallocate(void *phead) {
((obj *)phead)->next = m_FreePosi;
m_FreePosi = (obj *)phead;
}
private:
//写在类内的结构,这样只让其在类内使用
struct obj {
struct obj *next;
};
int m_sTrunkCout = 5;
obj* m_FreePosi = nullptr;//始终指向下一个即将分配出去的内存
};
//怎么使用这个专门的myallocator类呢?假设Teacher38是要用的类
class Teacher38 {
public:
int leixing;
int jineng;
public:
static myallocator myalloc;//声明静态成员变量
//重写 operator new
static void* operator new(size_t size) {
return myalloc.allocate(size);
}
//从写 operator delete
static void operator delete(void *phead) {
return myalloc.deallocate(phead);
}
};
myallocator Teacher38::myalloc;//定义静态变量
void main() {
Teacher38 *tea[20];
//这里为什么用 ++i,而不是i++呢?效果一样,不同的是 ++i会右值,会少产生一次中间变量
for (int i = 0; i < 15;++i) {
tea[i] = new Teacher38();
printf("tea[%d] = %p\n", i, tea[i]);
}
for (int i = 0; i < 15; ++i) {
delete tea[i];
}
}
tea[0] = 034971F0
tea[1] = 034971F8
tea[2] = 03497200
tea[3] = 03497208
tea[4] = 03497210
tea[5] = 03495F48
tea[6] = 03495F50
tea[7] = 03495F58
tea[8] = 03495F60
tea[9] = 03495F68
tea[10] = 034970A8
tea[11] = 034970B0
tea[12] = 034970B8
tea[13] = 034970C0
tea[14] = 034970C8