1、引言
1.1自主内存管理机制来源
在我们实际的使用过程中如果出现了频繁new和delete,会出现以下问题:
- 效率不高
- 外碎片问题
【补充】——内碎片和外碎片
外碎片:内存分配后,释放后该内存没有办法被重新释放。
内碎片:由于内存对齐而浪费的一些内存,比如下面的这个结构体,其中就浪费了三个字节的内存
struct Data
{
int al;
char b1;
};
所以我们要针对这两点的不足,自己实现一个内存管理机制。
1.2静态链表的管理方式
1、原理
数组的形式存在,做链表的处理
2、操作过程
1、开辟空间
首先,将整个内存划分成一个一个数组,接下来把数组划分成一个一个的结点,如下图所示:
2、初始化
- 牺牲0号下标的空间,做未分配出去的链表的头结点
- 牺牲最后一个下标的空间做已分配出去的链表的头结点
3、插入数据
未分配链表获取一个结点插入到已分配链表中。
【举个栗子】
比如说此时插入数字-1。就把1号下标的数据域置为1,然后让0号下标的游标域指向2号,5号下标的游标域指向1号。最后将1号下标的游标域置为-1,如图所示:
再插入1的时候就从未使用部分的第一个结点开始重复上面的操作。但是是让2号的游标域来标记2,如下图所示:
4、删除元素
已分配链表中删除一个结点插入到未分配链表中
【举个栗子】将1号下标元素的内存释放,就要将其在已使用部分移除,添加到未使用部分。
2、实现一个简单的链队内存池
2.1内存池的管理方式
1、优化
有了上面关于静态链表的管理方式的理解,内存池的管理方式在其基础上做了优化,优化如下:
- 组织成不带头结点的单链表
- 不需要对已分配的结点,通过一个链表做统一管理。因为这里面的内存块是针对外部的一个变量进行管理的,所以说这个内存在外部有标识单元来标识的,所以就不用考虑以分配结点,也就是谁new就谁来用。
2、结构的处理
1、初始化
分为两个区域,一个是数据域,一个是指针,用pool指针统一指向
2、在外部new了一个内存
首先看有没有内存供外部使用,如果有的话,就在整个内存池里面删除一个结点供外部使用,然后pool指针指向下一个未被使用的结点。指针域只做链表的管理,不做数据域来使用的。
3、外部delete
这时就需要归还给内存池,只需要把该结点再插入到内存池就可以了
【举个栗子】
int* p = new int
* p = 20;
int* q = new int;
*q = 30;
delete q;
就是q所指向的位置指向poll当前所指向的位置。然后再让poll指向q所在的位置。具体如下图所示:
2.2链队列的结点类
结点的大体框架如下:
const int Node_count = 10;
template<typename T>
class Node
{
public:
Node(T val=T())
:mdata(val),pnext(NULL)
{}
void* operator new(size_t size);
void operator delete(void* ptr);
private:
template<typename T>
friend class Queue;
T mdata;
Node<T>* pnext;
static Node<T>* pool;
};
其中,包含数据域和指针域两个部分。因为在申请的时候是频繁的new队列里面的结点。所以在结点里面实现new、delete运算符的重载函数。
1、生成内存池的结构
就如上述框架里面的static Node<T>* pool;
静态的成员变量在所有类中是共享的,以此来实现一个pool,并在类外初始化.
2、new运算符重载函数
开辟空间
:当pool为空的时候就需要开辟出内存块。其中size代表结点的大小。每次开辟const int Node_count个结点出来做初始化
:通过指针加偏移的方式指向下一个结点。让各个结点的指针域都指向下一个结点的位置。开辟内存
:将第一个位置头删来实现供外部使用,给出一个标识表示开始位置,然后让pool移动
void* operator new(size_t size)
{
if