关于malloc、free、realloc的一些奇葩问题

        在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?

        目前的水平也只能摸索到这里了,这个问题希望能在日后的学习中得到证实、解决。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在C语言中,动态内存分配与释放是非常常见的操作。C语言提供了四个函数来实现动态内存分配与释放,分别是malloc、calloc、reallocfree。 1. malloc函数 malloc函数用于在内存堆中分配一块指定大小的内存空间,并返回其首地址。其基本语法如下: ```c #include <stdlib.h> void* malloc(size_t size); ``` 其中,size_t是一个无符号整型类型,用于表示需要分配的内存空间大小(以字节为单位)。malloc函数返回一个void类型的指针,指向分配的内存空间的首地址。如果分配失败,则返回NULL。 例如,以下代码用于在内存堆中分配10个int类型的数组空间: ```c int *p = (int *) malloc(10 * sizeof(int)); ``` 2. calloc函数 calloc函数用于在内存堆中分配一块指定大小的内存空间,并将其清零,然后返回其首地址。其基本语法如下: ```c #include <stdlib.h> void* calloc(size_t num, size_t size); ``` 其中,num和size都是size_t类型的整数,用于表示需要分配的内存空间大小(以字节为单位)。calloc函数返回一个void类型的指针,指向分配的内存空间的首地址。如果分配失败,则返回NULL。 例如,以下代码用于在内存堆中分配10个int类型的数组空间,并将其清零: ```c int *p = (int *) calloc(10, sizeof(int)); ``` 3. realloc函数 realloc函数用于重新调整已经分配的内存空间的大小,可以扩大或缩小内存空间。其基本语法如下: ```c #include <stdlib.h> void* realloc(void* ptr, size_t size); ``` 其中,ptr是需要重新调整大小的内存空间的首地址,size是需要调整的大小(以字节为单位)。realloc函数返回一个void类型的指针,指向调整后的内存空间的首地址。如果调整失败,则返回NULL。 例如,以下代码用于将之前分配的10个int类型的数组空间扩大为20个int类型的数组空间: ```c int *p = (int *) malloc(10 * sizeof(int)); p = (int *) realloc(p, 20 * sizeof(int)); ``` 4. free函数 free函数用于释放之前分配的内存空间,将其归还给内存堆。其基本语法如下: ```c #include <stdlib.h> void free(void* ptr); ``` 其中,ptr是需要释放的内存空间的首地址。调用free函数后,该内存空间将不再可用,程序不能再访问该内存空间中的数据。 例如,以下代码用于释放之前分配的10个int类型的数组空间: ```c int *p = (int *) malloc(10 * sizeof(int)); free(p); ``` 需要注意的是,为了避免内存泄漏和野指针问题,程序员需要谨慎使用动态内存分配与释放,尽量避免不必要的内存分配和释放。同时,为了确保程序的稳定性和可靠性,程序员需要在使用动态内存分配时,充分考虑内存分配失败的情况,并做好错误处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值