malloc内存分配与free内存释放

本秂侑毒 提交于 2019-11-30 05:48:55
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/diaozaoxiang/article/details/52433271

        这里的存储分配程序,讲的就是标准库中malloc函数的实现原理。首先要了解针对malloc的内存存储结构。malloc不像全局变量一样,不是在编译器编译的时候就会分配内存空间,而是在调用到malloc函数时才会分配空间。有时还会中途调用free函数释放空间出来。所以:

        1、malloc在第一次被调用时,从系统中获取最小为一个单元的空闲空间(eg:最小单元为1024个最受限单元块。当x<=1024,获取1024个,否则获取x个),再进行分配;

        2、malloc所剩下的空闲空间一般都不是连续的,而是分散的。这样也提高了空间的利用率。

        为了管理malloc的空闲空间,每一个独立块的最前面都包含了一个“头部”信息:一个指向下一个空闲块的指针、一个本身独立块的长度(书上说还有一个指向自身存储空间的指针,但每个存储空间都有自身的指针,为什么还要这个呢。后看英语版原著,这么写的:Each block contains a size, a pointer to nextblock, and the space itself.)。下一个空闲块是按存储地址升序排列,离本空闲块最近的一个空闲块,若本空闲块在最后,则指向最前的空闲块。这样所有属于malloc的空闲空间都被串在了一起。如下图所示:



        因为后面的free函数已经把相邻的空闲链表给整合成一块了,所以我的图没有出现相邻的空闲链表。

        每块由malloc控制的空间都包含一个“头部”信息,为了方便管理,每块的空间大小都是头部大小的整数倍。而头部长度=指向下一个空闲块的指针的长度+自身空间大小的unsigned长度。但为了确保由malloc函数返回的存储空间满足将要保存的对象的对齐要求。每个机器都有一个最受限的类型:如果最受限的类型可以存储在某一个特定的地址中,则其他所有的类型也可以存放在此地址中。有的是double型,有的是long型,甚至还有的是int型。因此,头部结构将与最受限的类型进行联合,来确保对齐。

        因为有头部信息,头部信息里的本块空间size也是包括头部的大小,所以每次申请malloc空闲块的时候,都要加上一个单元,最后返回给用户的时候,再去掉头部。

        每次调用malloc申请空间时,malloc有一个专门指向当前空闲块链表的静态指针freep。从当前开始扫描剩下的空闲块链表,直到扫到一个足够大的空闲块。此算法成为“首次适应”(first fit),与之相对的是“最佳适应”(best fit):它将扫描出满足条件最小的块。这里的代码是“首次适应”算法。结果将出现三种情况:

        1)找到一块刚好合适的空闲块,则此块空间从链表中移走并将此块的地址返回给用户,并把静态指针freep指向前一空闲块地址;

        2)找到一块比需求大的空闲块,则从此空闲块中的后部取一块与需求一样的空间给用户,前部改变空闲块大小便可;


注(直到写本博文才发现自己错了):

①一直都认为返回给用户地址前一单元的头部,应该把空间退还给前面的空闲块,不然就闲着了。其实,里面记录了一个重要信息:空间块大小(包括头部和返回给用户的单元大小),在free释放空间的时候,就必须用到此头部的信息;

②后面的free程序以为是专供系统申请空间后插入空闲块链表用的,其实它就是我们平常用malloc、realloc、或calloc申请空间后,再释放的程序。

        3)如果扫描了一遍,都没有找到足够大的空闲块,则向系统再申请一块新的空间。

        上面都是在malloc已经有了空闲块的前提下,但第一次申请的时候,malloc是没有空闲块空间的。因此,在预编译时,就建立了一个单元的空闲块链表base来当做空闲链表的入口。当第一次调用malloc时,空闲链表的静态指针freep为NULL,那将它指向base,大小设为0(这样这块base空间将一直存在,且不被申请,确保了之后freep一直指向有效的空闲块链表),且指向它自己,同时向系统申请空闲空间(每次向系统申请的空间都是一块连续的空闲块)。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. typedef long Align; /*按照long类型的边界对齐,即以long作为最受限类型*/  
  2. union header{   /*头部信息*/  
  3.     struct {  
  4.         union header *ptr;  /*指向下一个空闲块*/  
  5.         unsigned size;      /*本空闲块大小*/  
  6.     }s;  
  7.     Align x;    /*强制对齐*/  
  8. };  
  9. typedef union header Header;  
  10.   
  11. static Header base; /*第一次调用malloc的空闲块链表入口,大小为0的空链表(按照上面逻辑的来说,这里的size应该为1)*/  
  12. static Header *freep = NULL;    /*静态的空闲块链表指针,初始化为NULL。第一次申请后才会指向base*/  
  13.   
  14. /*malloc函数:通用存储分配函数*/  
  15. void *malloc(unsigned nbytes)  
  16. {  
  17.     Header *p,*prevp;   /*定义一个当前空闲块指针变量,和前一个空闲块指针变量*/  
  18.     Header *morecore(unsigned); /*用于向系统申请空闲空间函数*/  
  19.     unsigned nunits;    /*需要申请的实际单元大小,即上面图中的z*/  
  20.   
  21.     nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;  /*与上图对应,把字节大小转换为单元大小,向上取整,并加上一个单元(头部)*/  
  22.     if((prevp = freep) == NULL){    /*没有空闲链表,第一次申请*/  
  23.         base.s.ptr = prevp = freep = &base; /*freep指向base,base的下一个空闲块指针指向自己*/  
  24.         base.s.size =0; /*设置大小为0*/  
  25.     }  
  26.     for(p = prevp->s.ptr;;prevp = p, p = p->s.ptr){  
  27.         if(p->s.size >= nunits){  
  28.             if(p->s.size == nunits)  /*大小刚好合适*/  
  29.                 prevp->s.ptr = p->s.ptr;  /*移走此块空闲区域*/  
  30.             else{                   /*比实际需求大,从空闲块尾部分配*/  
  31.                 p->s.size -= nunits; /*缩小空闲块大小*/  
  32.                 p += p->s.size;  /*指针指向被申请的空间的头部*/  
  33.                 p->s.size = nunits;      /*设置被申请的空闲块大小*/  
  34.             }  
  35.             freep =prevp;   /*当前静态指针指向前一空闲块,如果当前块还有空闲区域,下次将继续从此处开始扫描,节省时间*/  
  36.             return (void *)(p+1);   /*返回去头部单元的空闲空间*/  
  37.         }  
  38.         if(p == freep)  /*闭环的空闲链表,第一次调用malloc申请,或扫描一遍,未发现足够大的空间*/  
  39.             if((p = morecore(nunits)) == NULL)  /*向系统申请空间*/  
  40.                 return NULL;    /*未申请成功,*/  
  41.     }  
  42. }  

        通过下面的morecore()和free()函数的程序分析可知,在向系统成功申请空间后,p将指向有足够空间的空闲块。但在此代码中,进入下一此空闲块扫描前,p将指向下一块不足的空闲块,导致多扫描了一遍。个人觉得,如果空间足够,可以多申请一个静态指针beforefreep,指向freep的前一个空闲块。这样上面代码可添加一句,提高效率:

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. if(p == freep){ /*这样要添加大括号*/  
  2. if((p = morecore(nunits)) == NULL)  
  3.         return NULL;  
  4.     p = beforefreep;  
  5. }  
  6. 或  
  7. if(p == freep) /*这里可以不用加大括号,else与最近的if匹配*/  
  8. if((p = morecore(nunits)) == NULL)  
  9.         return NULL;  
  10.     else  
  11. p = beforefreep;  

        向系统申请空间的时,不是按需分配,而是有一个最小申请单元数。让您足够用,这次用不完可以留着下次用,不用每次都向系统申请,又不会系统浪费空间。

        真正向系统申请空间,还需调用系统调用sbrk(n)(UNIX下),若申请成功,该指针返回指向n个字节的存储空间;若申请失败,返回-1(不是NULL)。返回的指针类型是char *(应该是最小的存储空间单元)。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. #define NALLOC 1024 /*最小申请单元数*/  
  2.   
  3. /*morecore函数:向系统申请更多的存储空间*/  
  4. static Header *morecore(unsigned nu)    /*返回的是静态空闲块链表指针*/  
  5. {  
  6.     char *cp, *sbrk(int);  
  7.     Header *up;  
  8.   
  9.     if(nu < NALLOC)  
  10.         nu =NALLOC;  
  11.     cp = sbrk(nu * sizeof(Header)); /*调用系统调用申请系统空间*/  
  12.     if(cp == (char *) -1)  
  13.         return NULL;    /*申请失败,没有空间*/  
  14.     up = (Header *)cp;  /*转换为Header*指针类型*/  
  15.     up->s.size = nu; /*设置此空间块的大小*/  
  16.     free((void *)(up +1));  /*释放空间*/  
  17.     return freep;     
  18. }  

        这里的返回的freep,在free中更新了,才返回的。当初也想过既然freep都是静态全局变量了,那这里为什么还要返回一个静态变量呢,直接在函数里赋值就好了。其实这里有成功与失败,所以程序来需要判断申请结果,而且返回的freep是与申请最相关东西。

        free(void *ap)函数就是释放指针ap所指的空间,具体要释放的大小在ap前一个指针,即头部信息里。释放主要就是为了把此空间插入到空闲块链表中。所以要找到此空间块两边的空闲块(也有可能只有一块空闲块,即入口base)。然后判断是否与前一块相连,与后一块相连,相连的话,合并成一块,否则直接在中间插入一个新的空闲块链表。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. /*free函数:释放ap,将ap块放入空闲链表中*/  
  2. void free(void *ap)  
  3. {  
  4.     Header *p, *bp;  
  5.   
  6.     bp =(Header *)ap -1;    /*指向ap块的头部*/  
  7.     for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)    /*找到bp所在空闲链表中的位置*/  
  8.         if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))   /*判断是否在链表的开头或末尾*/  
  9.             break;  
  10.   
  11.     if(bp + bp->s.size == p->s.ptr){  /*先判断能否与高地址的空闲块合并,即与后一块合并*/  
  12.         bp->s.size += p->s.ptr->s.size;  
  13.         bp->s.ptr = p->s.ptr->s.ptr;  
  14.     }  
  15.     else  
  16.         bp->s.ptr = p->s.ptr; /*不能合并,bp指向后一块地址*/  
  17.   
  18.     if(p + p->s.size == bp){ /*再判断能否与地地址的空闲块合并,即与前一块合并*/  
  19.         p->s.size += bp->s.size;  
  20.         p->s.ptr = bp->s.ptr;  
  21.     }  
  22.     else  
  23.         p->s.ptr =bp;    /*不能合并,p指向bp地址*/  
  24.     freep =p;  
  25. }  

注:中文版翻译的又有歧义了,原著分别是“join to upper nbr”和“jointo lower nbr”。


        这个free程序,处理的太妙了。一般思维,先与前一块合并,再与下一块合并,如下面的程序(显然比我的好多了):

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. if(p + p->s.size == bp){ /*与前一块相连?*/  
  2.     if(bp + bp->s.size == p->s.ptr){  /*与后一块相连?*/  
  3.         p->s.size += bp->s.size + p->s.ptr->s.size;  
  4.         p->s.ptr = p->s.ptr->s.ptr;  
  5.     }else  
  6.         p->s.size += bp->s.size;  
  7. }else{  
  8.     if(bp + bp->s.size == p->s.ptr){  /*与后一块相连?*/  
  9.         bp->s.size += p->s.ptr->s.size;  
  10.         bp->s.ptr = p->s.ptr->s.ptr;  
  11.         p->s.ptr = bp;  
  12.     }else   /*不与任何一块相连*/  
  13.         bp->s.ptr = p->s.ptr;  
  14.         p->s.ptr = bp;  
  15. }  

         从这里可以看出,通过malloc申请后的空间,并没有初始化,所以在使用前记得初始化,不小心当做右值使用,出错的概率很大。

        《C程序设计语言》(第2版·新版)不愧是经典,值得细读和巩固。感谢作者!

        写了很久才写完,有错误或不好的地方,欢迎各位指正和批评!也感谢您花时间阅读!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!