一、概述
1、malloc简介
函数所在头文件:
函数原型是:void *malloc (size_t n)
函数功能:在内存的动态存储区中分配一个长度为size的连续空间。其参数是一个无符号整形数,返回值是一个指向所分配的连续存储域的起始地址的指针。
2、malloc函数使用注意事项
- 申请了内存空间后,必须检查是否分配成功。
- 当不需要再使用申请的内存时,记得释放;释放后应该把指向这块内存的指针指向NULL,防止程序后面不小心使用了它。
- malloc和free函数应该配对使用。如果申请后不释放,就是内存泄露;如果无故释放那就是什么也没有做。释放只能一次,如果释放两次及两次以上会出现错误。
- 越界使用动态分配的存储块,尤其是越界赋值,可能引起非常严重的后果,通常会破坏程序的运行系统,可能造成本程序或者整个计算机系统垮台。
3、本文的目的
一直很想亲手实现一个malloc函数,不仅仅是锻炼自己的编程能力,对指针的驾驭能力,而且也是为了解除自己对malloc函数的疑惑--为什么使用malloc函数有上述的注意事项?要想知道原因,只有自己查阅源码,或者自己编写功能类似的源码,而笔者选用了后者。
二、本程序实现机制
1、块控制头部
块控制头部结构体定义如下所示:
typedef struct{unsigned char is_available; /* whether blcok is avaiable */unsigned int prior_blocksize; /* size of prior block */unsigned int current_blocksize; /* block size */}mem_control_block;
每申请一个内存空间,无论大小(char、short、long)都将其命名为"Block"。在分配一个Block的时候,在堆区(heap)首先存放的是Block的控制块,然后才是有效的数据区。可以想象,假设我们申请一个char型大小的空间,实际上在堆区占据了“sizeof(char)+sizeof(mem_control_block)”。就是通过Block的头部控制块将堆区连成一个链表,通过这个链表可以遍历整个堆区的每一个块。
is_avilable标记内存块是否被分配,是否可用。prior_blocksize保存了前一个块的大小,current_blocksize保存了当前块的实际有效数据空间大小。有了current_blocksize,我们可以从前向后遍历整个堆区;而有了prior_blocksize,我们可以从后向前遍历整个堆区。也就是说,这个控制块头部使我们为堆区建立了一个双向链表,以便于管理。
2、malloc
调用malloc函数时,它沿连接表从前向后,寻找一个没有被占用的,而且大到足以满足用户请求所需要的内存块。在寻找过程中,分三种情况。
第一种情况,倘若可用而且大小刚好合适,那么直接将此块返回就完成了分配。
第二种情况,倘若可用而且超过了所需要的内存块并且还能容纳一个新块的控制头部,这个时候就将此块一分为二。将分配给用户的那块内存的is_available置0(不再可用),然后将此块的有效数据区首地址作为函数值返回。而新块的控制头部要进行赋值(只有含有头部的块才能称的上一个块),没有控制头部的块不能进行管理。
第三种情况,倘若可用内存块的空间小于需要的空间,或者虽然超过了但是不能腾出一个控制头部,最终的处理都是跳过改块,移动到链表的下一块进行新一轮的比较。
3、free
free函数用于内存块的回收,当不用的内存块调用此函数释放空间的时候,将is_available置1(可以重新分配)。接下来要做的就是整合内存碎片。由于可能多次malloc函数,产生了很多的内存碎片,在释放一个块的时候,不仅要标记此块为“可用”。另外,还需要试着将此块前后的块进行合并,这样才能为将来申请更大的空间做好准备。
4、malloc_init
初始化函数,主要是确定堆区的起始位置、大小和结束地址,也就是确定堆区的边界。我们申请的内存块必须在堆区内,不得超出边界,否则会修改其他的数据。
另外,还需要对堆区进行初始化,建立第一个内存块(初始化头部控制结构体)。显然,这个块是一