目录
1.平时使用的 new 和 delete 有下面两个功能
- new 1.开辟空间 2.初始化 delete 1.释放资源 2.释放空间
- 而这两个函数的功能为: operator new 开辟空间 operator delete 释放空间
先给出一段简单的代码(operator new ,operator delete的简单自己实现):
#include <iostream>
using namespace std;//为 using 指示符,其中 namespace 为名字空间作用域
//建议不用这个指示符,可在需要的地方加 std 的作用域
void* operator new(size_t size) //size 代表总字节个数
{
void* ptr = malloc(size);
cout << "operator new: " << ptr << endl;
return ptr;
}
void operator delete(void* ptr)
{
cout << "operator delete: " << ptr << endl;
free(ptr);
}
int main()
{
int *p = new int;
delete p; //直接调用我们写好的 operator new(开辟空间) 和
//operator delete (释放空间),没有初始化和释放资源
return 0;
}
2.为什么要自己编写这两个函数?
主要是默认内存管理函数的不足(详细见下面3),简单总结来说,主要有以下两点:
- new作为用户程序的关键字,调用系统开辟空间的接口,系统API交给一个文件或者管理系统,操控底层的硬件资源BSP。
所以new的过程是从用户态到内核态,其缺点一 :效率低;
- 如果申请3个字节,系统将分配4个字节,导致1个字节不可用,所以频繁调用易产生碎片,这是缺点二。
所以这两个函数 operator new ,operator delete 实现了一个自己的内存管理方案(即内存池。池:一组资源的集合)
3.默认内存管理函数的不足
https://www.jianshu.com/p/46c40e368f9f
- malloc/free 和new/delete在堆上申请和释放内存都有一定的额外开销。
- 开销来自维护内存空闲块表。
- malloc和new申请堆内存时,首先查找内部维护的内存空闲块表,并且需要根据一定的算法(例如分配最先找到的不小于申请大小的内存块给请求者,或者分配最适于申请大小的内存块,或者分配最大空闲的内存块等)找到合适大小的空闲内存块。如果该空闲内存块过大,还需要切割成已分配的部分和较小的空闲块。然后系统更新内存空闲块表,完成一次内存分
- 类似地,在free和delete释放内存时,系统把释放的内存块重新加入到空闲内存块表中。如果有可能的话,可以把相邻的空闲块合并成较大的空闲块。
- 默认的内存管理函数还考虑到多线程的应用,需要在每次分配和释放内存时加锁,同样增加了开销。
- 可见,如果应用程序频繁地在堆上分配和释放内存,则会导致性能的损失。并且会使系统中出现大量的内存碎片,降低内存的利用率。
- 默认的分配和释放内存算法自然也考虑了性能,然而这些内存管理算法的通用版本为了应付更复杂、更广泛的情况,需要做更多的额外工作。而对于某一个具体的应用程序来说,适合自身特定的内存分配释放模式的自定义内存池则可以获得更好的性能。
4.内存池定义
内存池是一组资源的集合。程序在申请堆上的内存时,我们可以给它一大块内存(远超出程序要申请的大小,即内存池),并且自己管理,而不是它要多少就从切换到内核态,给它多少。
应用程序可以通过调用系统的内存分配函数预先一次性申请适当大小的内存作为一个内存池,并为这个内存池类或结构体定义一些分配和释放内存块的成员函数。
之后应用程序自己对内存的分配和释放则可以通过这个内存池类及其成员函数来完成。只有当内存池大小需要动态扩展时,才需要再调用系统的内存分配函数,其他时间对内存的一切操作都在应用程序的掌控之中。
5.内存池优点
(1)针对特殊情况,例如需要频繁分配释放固定大小的内存对象时,不需要复杂的分配算法和多线程/多进程保护(系统内存管理一直会加锁解锁)。也不需要维护内存空闲表的额外开销,只需维护简单的内存池块头信息,从而获得较高的性能。
(2)由于开辟一定数量的连续内存空间作为内存池块,因而一定程度上提高了程序局部性和数据访问的速度,提升了程序性能。
(3)比较容易控制页边界对齐和内存字节对齐,没有内存碎片的问题。
3.内存池的设计
怎样把资源分配出去?怎样把资源回收回来?内存池的设计如下:
用静态链表实现(静态链表,即用数组描述的链表);静态链表中基本单位是node结点;如图为6个结点,每个结点的指针域都存放下一个结点的地址,这样这6个结点就连在一起。node结点有数据域和指针域,数据域为T类型(T为模板),指针域为 node* 类型。
内存池的精髓在于内存池自己管理这些结点:即做好 operator new ,operator delete 的任务。
3.1申请:
归还第一个:
3.2删除(再归还第三个)
3.3用完后的扩容
如果再归还之前内存池中的某个node,如图:
4.代码
//结合链队写一个简单内存池(意义不大) 之后再写一个通用内存池
const int MEM_POOL_SIZE = 10;
template<typename T>
class Queue//只有在new QueueItem()才会开辟内存池
{
public:
Queue()
{
pfront = prear = new QueueItem(0);
}
~Queue()//链怎么释放节点
{
QueueItem* pCur = pfront;
QueueItem* pNext = pfront;
while(pCur != NULL)
{
pNext = pCur->pnext;
delete pCur;
pCur = pNext;
}
pfront = prear = NULL;
}
void push(T val)//入队
{
QueueItem* pnewnode = new QueueItem(val);
prear->pnext = pnewnode;
prear = pnewnode;
}
void pop()//出队
{
if(!IsEmpty())
{
QueueItem* pCur = pfront->pnext;//有头结点,pCur指向第一个数据节点
pfront->pnext = pCur->pnext;//让头结点指向第二个数据节点
delete pCur;
}
}
T front()//获取队头元素
{
if(!IsEmpty())
{
return pfront->pnext->mdata;
}
}
T back()//获取队尾元素
{
if(!IsEmpty())
{
return prear->mdata;
}
}
private:
bool IsEmpty()
{
return pfront == prear;
}
class QueueItem
{
public:
QueueItem(T val = T()):mdata(val),pnext(NULL){}
void* operator new(size_t size)
{
if(pool == NULL)
{
pool = (QueueItem*)new char[size * MEM_POOL_SIZE];
QueueItem* pCur = pool;
for(pCur;pCur < pool + MEM_POOL_SIZE - 1;pCur++)
{
pCur->pnext = pCur + 1;
}
pCur->pnext = NULL;
}
QueueItem* rt = pool;
pool = pool->pnext;
return rt;
}
void operator delete(void* ptr)
{
if(NULL == ptr)
{
return;
}
QueueItem* p = (QueueItem*)ptr;
p->pnext = pool;
pool = p;
}
public:
T mdata;//数据域
QueueItem* pnext;//指针域
static QueueItem* pool;
};
QueueItem* pfront;//队头指针
QueueItem* prear;//队尾指针
};
template<typename T>
typename Queue<T>::QueueItem* Queue<T>::QueueItem::pool = NULL;//静态变量一定在类外初始
// QueueItem类型(加关键字typename) QueueItem 是普通的类,不需要加模板类型参数
int main()
{
Queue<int> que;
for(int i = 0;i < 10;i++)
{
que.push(i+1);
}
return 0;
}
参考:https://blog.csdn.net/zDavid_2018/article/details/89303269