一.前言
大一刚开学,没什么事情干,随便找点东西写写学学看
emmm原有一个伟大的愿景,想玩个大的,但是越探索越觉得水深
有预感会半途而废,学多少是多少好了
当然如果能完成那再发欸嘿嘿嘿,现在发多少有点立了flag了
稍微写个小笔记吧。
二.原理
说回来,众所周知C++分配内存用的是new与delete,C老一点是malloc和free
然而在代码实现的时候,有可能多次分配内存
于是天天买生活用品我们受够了,决定不买散装的,直接买一个月的
就是类似于如此
三.架构与思路

如图,大致的结构就是如此,其中MemoryBlock就是我们想要的内存块,即内存单位;MemoryPool负责内存单元的管理和分配。为了实现动态增删,我们使用链表结构来储存MemoryBlock。若已分配\创建的内存单位不足,可随时增添;删除虽然麻烦点,但这个结构也是最适合内存处理的,(也许)后面会涉及。
四.实现
(1)MemoryPage
现在我们一部分一部分考虑,先从MemoryPage说起
如上图,我们创建一个MemoryPage类
class MemoryPage{
public:
int nSize;
int nFree;
int nFirst;
void* operator new(size_t,int Unitsize,int Unitnum);
void operator delete(void* pBlock);
MemoryPage(int Unitsize,int Unitnum);
~MemoryPage(){};
MemoryPage* pNext;
char aData[1];
};
其中
- nSize用于记录总大小
- nFree用于记录剩余空闲空间
- *pNext记录链表下一位
- aData用于记录内存单元可用初始指针,这个用了C99一个很骚的特性,不理解先看着结构即可
构造函数(与new的重载)中
- Unitsize:内存单元大小
- Unitnum:内存单元数量
在构造函数时要注意内存对齐问题,这个问题会在MemoryPool类得到处理(处理Unitsize大小)
ok,现在从构造函数说起
A.MemoryPage(int Unitsize,int Unitnum);
MemoryPage::MemoryPage(int Unitsize,int Unitnum):
nSize(Unitsize*Unitnum),
nFree(Unitnum-1),
pNext(NULL),
nFirst(1)
{
char* pData=aData;
for(int i=1;i<Unitnum-1;i++){
*(unsigned short*)pData=i;
pData+=Unitsize;
}
};
1.初始化:内存页构造完成后将总大小(nSize)设定为Unitsize*Unitnum,即单元大小*单元数量;
nFree设定为Unitnum-1(由于第一个单元已被aData征用)
pNext自然是NULL
nFirst用于记录内存的首位,初始为1
接下来是对aData的解释:
灵活数组成员(Flexible Array Member),一个C99标准中引入的骚东西。该特性允许在结构体中存放一个零长数组(此处长度为1,所以占用一部分内存,nFree=Unitnum-1),要求是该数组必须存放在结构体的最后一个成员,此时该数组可作为正常指针使用,可容纳任意数量的数据(只要你分配了内存)
看到好处没有?我们在编写之前一大难题就是,未分配的内存向系统申请后就这样放着吗?又该怎么访问并存储数据呢?
aData给出了答案,分配内存后,从头指针aData本身便可追溯到指定的内存地址。通过aData+Unitsize+所在的序号即可
那么赋值i又是为什么呢?此处我们用内存存储前两字节(short类型占两字节,范围0~65536)来存储序号,则我们能顺着链表找到我们想要的那块,而nFirst则是标明下一个空闲内存页的序号,方便访问和分配。于是我们完成了MemoryPage的构造。
B.new/delete的重载
void* MemoryPage::operator new(size_t,int Unitsize,int Unitnum){
return ::operator new(sizeof(MemoryPage)+Unitnum*Unitsize);
}void MemoryPage::operator delete(void* pBlock){
::operator delete(pBlock);
}
delete操作直接调用原来的delete即可
而new的重载则考虑所需的内存空间大小即sizeofof(MemoryPage)+Unitnum*Unitsize,即给MemoryPage内成员开好空间,接着继续开Unitnum*Unitsize的内存空间,这些空间的存储地址以aData指针为首位,进而继续有Unitnum*Unitsize空间的池容量
到这里,MemoryPage便构建完成
(2)MemoryPool
class MemoryPool{
public:
MemoryPool(int Unit,int Init=256,int Grow=1024);
~MemoryPool();
void* Allocate();
void Free(void* pFree);
private:
int Unitsize;
int Initsize;
int Growsize;
MemoryPage* pBlock;
};
接下来是MemoryPool类
MemoryPool作为一个链表头来访问管理和分配MemoryPage,以实现内存分配
其中Unitsize仍然是内存单元大小,Initsize是初始内存数,Growsize是增长的内存数,pBlock存储内存块链表头,Allocate与Free则是内存分配与释放。
A.MemoryPool(int Unit,int Init=256,int Grow=1024);
MemoryPool::MemoryPool(int Unit,int Init,int Grow):
Unitsize(Unit),
Initsize(Init),
Growsize(Grow),
pBlock(NULL)
{
if(Unit>4)Unitsize=(Unit+7)&~7;
else if(Unit>2)Unitsize=4;
else Unitsize=2;
}
按理来说MemoryPool构造没什么好讲的,初始化参数即可,但偏偏有一个很影响功能的问题
内存对齐
为什么要内存对齐
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
- 假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器。这需要做很多工作。
- 现在有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。
此处同样如此
那么如何对齐呢?仔细一想,存储时内存大小由我们决定,要使内存存储在对应的的系统内存上,只需要对内存大小进行一些小处理,也就是让其存储与4/8等2次幂的倍数上。于是有这一条
- if(Unit>4)Unitsize=(Unit+7)&~7;
即将Unitsize提升到8的倍数
但为什么小于等于4时也需处理呢?
避免浪费
譬如一个长度为3的空间需求,若按8的倍数需要分配8的内存,此时有67.5%的内存被浪费了
于是此时我们分情况对齐内存即可。
B.MemoryPool::~MemoryPool()
MemoryPool::~MemoryPool(){
MemoryPage* tmp=pBlock;
while(tmp!=NULL){
MemoryPage* Del=tmp;
tmp=tmp->pNext;
delete(Del);
}
}
很好理解,遍历链表,一一删除释放即可
C.void* MemoryPool::Allocate()
void* MemoryPool::Allocate(){
if(pBlock==NULL){
pBlock=(MemoryPage*)new(Unitsize,Initsize) MemoryPage(Unitsize,Initsize);
return pBlock->aData;
}MemoryPage* tmp=pBlock;
while(!tmp->nFree)tmp=tmp->pNext;
if(tmp==NULL){
if(Growsize==0)return NULL;
tmp->pNext=(MemoryPage*)new(Unitsize,Growsize) MemoryPage(Unitsize,Growsize);
tmp=tmp->pNext;
return (void*)tmp->aData;
}tmp->nFree--;
char* ptmp=tmp->aData+tmp->nFirst*Unitsize;
tmp->nFirst=*ptmp;
return (void*)ptmp;
}
对于分配的解释会冗长一点
首次分配时先为pBlock分配初始内存同时调用构造函数,再将首地址返回
若是再次分配,则遍历链表,找到空闲单元(nFree>0),并根据nFirst返回可用的内存指针,然后对nfirst等进行处理
若是nFree=0,那就需要新开一个单元,根据Growsize内存数新分配单元再返回首地址即可访问,最后返回的是void*指针
D.void MemoryPool::Free(void* pFree)
void MemoryPool::Free(void* pFree){
MemoryPage* ptmp=pBlock;
MemoryPage* pre=NULL;
while(ptmp!=NULL&&(ptmp->aData>pFree||ptmp->aData+ptmp->nSize<pFree)){
pre=ptmp;
ptmp=ptmp->pNext;
}if(ptmp==NULL){
throw "404";
return;
}*(int*)pFree=ptmp->nFirst;
ptmp->nFirst=((char*)pFree-ptmp->aData)/Unitsize;
ptmp->nFree++;
if(ptmp->nFree==ptmp->nSize/Unitsize){
if(pre==NULL)pBlock=ptmp->pNext;
else pre->pNext=ptmp->pNext;
delete ptmp;
}else{
pre->pNext=ptmp->pNext;
ptmp->pNext=pBlock->pNext;
pBlock=ptmp;
}
}
接着是释放内存
Free函数获取一个已分配的指针,接下来去找到指针所属的内存单元,找不到就抛出异常,接着考虑释放该内存此时要做一个判断
- 若是该内存单元所有内存全部清空,则将该内存单元释放清空
- 若是去除后仍有内存单元,则将nFirst改变,并将该内存单元移动至链表头部(其实没什么必要),然后继续运作
五.改进的可能
首先就是MemoryPage可能能够通过内存对齐提升性能,AI说的不大清楚,其实MemoryPool的内存对齐应该已经够了
其次便是MemoryPage,注意到了吗,该内存池实际上是个定长内存池,即每个内存单元的长度统一且固定,感兴趣的可以了解一下变长内存池,这类内存池能够根据需求变化内存单元大小
okk,完结撒花

Comments | NOTHING