在man手册中可以看到这样的声明:
#include <stdlib.h>
void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
用malloc.h头文件也可以,没有查出来在功能上二者有什么太大的区别,不过ANSI C中建议去使用stdlib.h而不是malloc.h,在linux下的man手册中也可以看出,malloc包含于stdlib.h中。
这四者的功能分别为:
malloc:在内存的动态存储区中分配一块长度为size 字节的连续区域。假如请求去分配一块大小为100字节的内存空间,那么它实际分配的内存就是一100个连续的字节,并不会分开位于两块或多块不连续的内存。同时,malloc实际分配的内存会比你请求分配的内存大小大一些(后文将提到)。malloc的返回值为该区域的首地址。如果无法向malloc提供请求的size字节大小的内存的话,将返回一个NULL指针。因此,对每个从malloc返回的指针都进行检查,确保它不为NULL是很有必要的。以往的malloc返回值类型是(char *)型,ANSI C中将其改为了(void *)型,可以转换为其它任何类型的指针。
free:释放ptr指向的存储空间。被释放的空间通常被送入可用存储区池,以后可在调用malloc、realloc以及calloc函数来再分配。free的参数要么是NULL,要么是先前从malloc、calloc或realloc返回的值。向free传递一个NULL不会有任何效果。
calloc:在内存的动态存储区中分配n个长度为size的连续空间,函数返回一个指向分配起始地址的指针;如果分配不成功,返回NULL。跟malloc的区别:(1)、calloc在动态分配完内存后,自动初始化该内存空间为零,而malloc不初始化,里边数据是随机的垃圾数据。如果你写的程序只是想要把一些值存到数组中,那么这个初始化过程纯属是在浪费时间。(2)、二者请求内存数量的方式不同。calloc需要两个参数,即所需元素的数量和每个元素的字节数。根据这两个参数来计算出总共需要分配的内存空间大小。
realloc:先判断当前的指针是否有足够的连续空间,如果有,扩大ptr指向的地址,并且将ptr返回,如果空间不够,先按照size指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来ptr所指内存区域(注意:原来指针是自动释放,不需要使用free),同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。
有一个问题来了:
我们来看一下free函数的原型:void free(void *ptr);
参数只有一个:*ptr。假定ptr已经指向了一块大小为100的内存块,那么我们将指针ptr作为参数传递给free函数时,free函数是怎样仅凭一个指针来工作的呢?它是怎样保证,被释放掉的空间正正好好是100字节,而不是1字节,4字节,8字节呢?
下面就涉及到了上文中提到的使用malloc实际分配的内存大小要比size大小稍大的原因:
在讲座的时候,刘欢学长提到了这个问题,他的解释是:在调用malloc函数去申请内存空间的时候(假设申请100字节空间),在返回的指针的前方的一小块空间里,会保留着这块空间的大小信息,所以将该指针传递给free时,free仅需将指针前溯,便可以找到这块内存空间的信息,从而能进行精确的释放。
申请的时候实际上占用的内存要比申请的大。因为超出的空间是用来记录对这块内存的管理信息。在《UNIX环境高级编程》中第七章第八节有这样一段话:
“大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息——分配块的长度,指向下一个分配块的指针等等。这就意味着如果写过一个已分配区的尾端,则会改写后一块的管理信息。这种类型的错误是灾难性的,但是因为这种错误不会很快就暴露出来,所以也就很难发现。将指向分配块的指针向后移动也可能会改写本块的管理信息”。
为了验证这个问题,上网搜索了好久,找到一些相关信息,说法大致相同:
为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构:
struct mem_control_block
{
int is_available; //标记空间是否可用
int size; //记录空间大小
};
在调用malloc来返回一个指针之前,系统会将指针移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。这样,指针指向的便全部是空闲的、开放的内存。当通过 free函数将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。从而实现精确地释放malloc所申请的大小的内存空间。
对于free的这种工作机制,这个解释方法倒是很可信,但是我还是有一个疑问,倘若每块经malloc申请的内存空间都是用这样一个mem_control_block结构体来保存空间信息,那么用malloc申请来的空间来存放数据岂不是很不安全?如果不巧哪里发生了数据溢出,正好覆盖到了mem_control_block上,那么后果是不是有些太严重?还是有一个更为安全的存储方法?
又想到一个比较有趣的问题:
在做课程设计的时候,偶然遇到过这样一种情况:在一个单链表中,已经进行了free(p)操作,却意外地发现指针p仍然可以被正常使用。刚刚又现写了一个单链表,照旧,进行free(p)操作,发现仍可以正常操作的不仅仅有p,甚至p->next都可以正常操作:
#include <stdio.h>
#include <stdlib.h>
typedef struct linklist
{
int data;
struct linklist *next;
}LB;
LB *creat(void)
{
int i;
LB *head,*p,*q;
head = (int *)malloc(sizeof(LB));
p = (int *)malloc(sizeof(LB));
head->next = p;
scanf("%d",&p->data);
for(i = 0;i < 3;i++)
{
q = (int *)malloc(sizeof(LB));
scanf("%d",&q->data);
p->next = q;
p = p->next;
}
p->next = NULL;
return head;
}
int main( int argc,char *argv[] )
{
LB *p;
p = creat();
p = p->next;
free(p);
printf("output:%d\n",p->next->data);
return 0;
}
运行结果:
1
2
3
4
output:2
和上面的内容联系起来,这个现象便不难以解释:
free(p)的言外之意就是告诉编译器:attention,attention!这块内存从现在开始我不用了,如有需要随便拿去用。而p就是这块内存的起始地址,free(p)进行的操作就是把mem_control_block结构体里面的is_available标记为“可用”,而里面的数据此时已被抛弃。但p没有置为空,那还是原来的那个值。只是里面的内容已不受控,有可能不会变,有可能会被改写覆盖,结果此时便是未知的。所以上面的结果能正常输出“2”也便是不一定的。
所以在用free进行释放内存时,最好加上一句p = NULL。
在《C陷阱与缺陷》中,还提到了一个关于realloc的这样的问题:
在UNIX系统参考手册第7版中这样来描述realloc的行为:如果ptr指向的是一块最近一次调用malloc、realloc或alloc分配的内存,即使这块内存已经被释放,realloc仍可以正常进行工作。
也就是说,realloc可以在某内存块被释放后重新分配其大小。于在在我的x86,gcc 4.7.2中做了这样一个测试:
#include <stdio.h>
#include <stdlib.h>
int main( int argc,char *argv[] )
{
int *p;
p = (int *)malloc(sizeof(int));
*p = 5;
printf("%d\n",*p);
free(p);
p = (int *)realloc(p,8);
printf("%d\n",*p);
return 0;
}
运行结果:
5
0
结果发现程序果然可以正常编译通过,realloc也可以正常工作。这也说明realloc可以接受一个已经被“抛弃”了的指针p。
上面讨论free的时候,已经说明了经free后的内容已不再受控,虽然在单链表的测试中,p->next仍然可用,但是仍不保险,因为它的结果是未知的。
《C陷阱与缺陷》中(P116)提供了这样一个“技巧”,可以来保证p->next在被free后仍有效:
for(p = head;p != NULL;p = p->next)
{
free((char *)p);
}
问题又来了:
假设链表中的数据类型是int型,不知经这样的处理后,结构体中的size是否也会随之改变?如果会随之改变,那么的确可以保往链表每个结点中的指针域。但是仅仅做了free((char *)p)这一个处理,真的能把mem_control_block中的4改为1?
目前的水平也只能摸索到这里了,这个问题希望能在日后的学习中得到证实、解决。