• 流言终结者——C语言内存管理


    写在前头:
    我不能保证此文中,我的观点和理解全是对的,这也不是一篇教学贴,只是我偶尔突发奇想了几个特殊的场景,然后用实验得到结果,对结果进行分析,遂成此文。所以文中肯定存在错误,我也没想到会上首页,引来众人围观。
    最后,欢迎拍砖,我觉得错了不要紧,改就是了,最惨的是不知道自己错在哪。

    首先看一下man手册中的定义,

    void *malloc(size_t size);
    向系统申请size个Bytes长的连续内存,返回一个void类型的指针,指向这块儿内存的首地址。这块申请到的内存是不洁的(也就是非全0x00,内容可以是任意的,随机的)
    如果size的值是0,那么返回的指针要么是指向NULL,要么是指向一个unique的地址,这个地址是可以被free释放的。(这里的解释是有问题的,例子(8)会证明)

    void free(void *ptr);
    释 放ptr指向的内存空间,ptr必须是之前调用过malloc,calloc,realloc这三个函数返回的,否则,如果free(ptr)已经执行过 了,而又执行一次,那么会导致意外发生(undefined behavior occurs.),如果ptr指向的是NULL,则不会做任何操作。

    (1)假设有

    1 char *p = NULL;
    2 p = (char*)malloc(0);

    那么p获得的内存块的长度到底是多少?能否往里面写入数据?
    答:不妨用这段代码来测试:

    01 int main(int argc, char **argv)
    02 {
    03     char *value = NULL;   
    04     char *ori   = NULL;
    05     value = malloc(0);
    06     ori   = value;
    07     printf("value[0] is [%c]\n", *value);
    08     while(1) {
    09         *value = 'a';
    10         value++;
    11         printf("value len [%d]\n", strlen(ori));
    12     }
    13 }

    这段代码结果如下所示:Fedora14:

    01 [michael@localhost mem-test]$ ./a.out    
    02 value[0] is []
    03 yydebug:[./mem-test.c]:[34]:value len [1]
    04 yydebug:[./mem-test.c]:[34]:value len [2]
    05 yydebug:[./mem-test.c]:[34]:value len [3]
    06 ...(省略N行)
    07 yydebug:[./mem-test.c]:[34]:value len [135157]
    08 yydebug:[./mem-test.c]:[34]:value len [135158]
    09 yydebug:[./mem-test.c]:[34]:value len [135159]
    10 Segmentation fault (core dumped)
    11 [michael@localhost mem-test]$

    我重新编译、运行了很多次,最后打印结果都是135159;
    这 个结果证明了malloc(0)返回的指针指向的是一个非NULL的内存地址处,并且该处的值是0x00,并且对于该进程,不仅该处是可写的,而且之后的 135159个Bytes也都是可写的,也就是属于这个进程空间。直到最后,可能写到别的进程的空间里面去了,才被内核发来段错误信号,自己结束了。

    其实在写该处就已经是内存越界了,往后继续写更加是内存越界,只是刚好那么巧,该处往后的内存块依然是该进程的有效heap区间,所以才没有被内核发段错误,而往往这种情况是最惨的,在实际开发工作中,假设哪天真的出个这情况,又不会被警告,但是却有发现数据被窜改。

    (2)假设有

    1 void *p = NULL;
    2 p = malloc(0);

    那么稍后p需要用free(p)来释放,以避免内存泄漏吗?
    答:不妨用这段代码来测试:

    01 int main(int argc, char **argv)
    02 {
    03     char *value = NULL;
    04  
    05     while(1) {
    06         value = (char*)malloc(0);
    07         printf("value addr [%p]\n", value);
    08     }
    09     return 0;
    10 }

    结果如下所示:

    1 ...(幸亏我及时Ctrl+C停住,运行超过几秒,电脑就会卡爆了)
    2 value addr [0x87aa5c8]
    3 value addr [0x87aa5d8]
    4 value addr [0x87aa5e8]^C
    5 [michael@localhost mem-test]$

    从打印看来,虽然是调用malloc(0);,但是每次指向的地址都不同,并且逐渐增大,偏移是0x10,也就是16个字节。

    可以不用while(1)来测试,用一个有限的不是很大的值来测试,然后用top命令来观察这个进程的资源使用情况,可以看出a.out消耗的内存在不断增加,所以答案就是,malloc(0)申请的内存,也要通过free()来释放,以避免内存泄漏。

    (3)假设有

    1 char *p = NULL;
    2 free(p);

    那么会导致进程退出吗?
    答案:不会,free(NULL)相当于啥事儿不干。


    (4)假设有

    1 char *p = NULL;
    2 while(1) {
    3     free(p);
    4 }

    那么会导致进程退出吗?
    答案:不会,进程永远循环在while(1)里面,不会出错退出。


    (5)假设有

    1 void *p = NULL;
    2 p = malloc(256);
    3 free(p);
    4 free(p);

    那么进程会出错退出吗?
    答案:进程会出错退出,打印堆栈信息,提示

    1 [michael@localhost mem-test]$ ./a.out
    2 *** glibc detected *** ./a.out: double free or corruption (fasttop): 0x09dad008 ***

    类似的信息。所以已经free掉的内存,除非又申请回来了,否则不能再次去free它,否则进程会出错。


    (6)为什么经常有人说free(p);要和p = NULL;一起用,以避免“野指针”的出现?
    答案:其实用上面那个例子(5)就能看出,如果free超过1次就会出错,但是从例子(3)和例子(4)可以看出,free(NULL);可以执行任意次 都不会出错。所以一般free(p);之后,马上把p指向NULL;,从而即使别人再去执行free(p);也不会出现错误。不仅如此,通过让p指向 NULL,也很好的给别人一个提示,你是否对p进行了成功的操作,让别人好判断。不妨看看如下的例子:

    01 void foo(char *in)//你做的功能函数
    02 {
    03     free(in);   
    04 }
    05 int main(int argc, char **argv)
    06 {
    07     char *p = NULL;
    08     p = strdup("hello_world");
    09     printf("p = [%s], len = [%d]\n", p, strlen(p));
    10     foo(p); //你同事在调用你的函数
    11  
    12     //感谢@<a href="http://my.oschina.net/louisluo" target="_blank" rel="nofollow">ColoredCotton</a>的贡献
    13     //由于foo函数的形参是*p,所以无法在foo函数内修改实参指针的指向,所以这的判断总是true
    14     if (NULL != p) {//他不确定你有木有free(),他还很聪明的做了一个判断
    15         free(p);
    16         p = NULL; //他习惯很好,free之后指向NULL
    17     }
    18     return 0;
    19 }

    假设foo函数是你写的,你同事在调用foo功能的时候,又不确定你是否free了传进去的那块内存,于是他就在调用完foo之后,加了判断,然后执行 free,结果他得到的结果是,虽然你free了指针p,但是这仅仅是告诉内核,这块内存我不用了,你可以收回了,值得注意的是,p依然指向这块内存,换 句话说也就是指针变量p存的值依然是刚才释放掉的那块内存的首地址。所以你同事的判断得到的结果是true,然后又会执行free(p);结果当然和例子 (5)一样了,如下所示:

    1 [michael@localhost mem-test]$ ./a.out
    2 p = [hello_world], len = [11]
    3 *** glibc detected *** ./a.out: double free or corruption (fasttop): 0x09088008 ***

    那么“野指针”还可以这样定义,指向所有非法地址(NULL除外)的指针都可以叫野指针。
    那么程序应该改成这样较妥:

    01 void foo(char **in)//调用方式应该是传入某个指针的地址
    02 {   
    03     printf(" &in = [%p], in\'s address\n", &in);
    04     printf("  in = [%p], in\'s value\n",  in);
    05     printf(" *in = [%p]\n", *in);
    06     printf("**in = [%c]\n", **in);
    07     free(*in); //*in是实参的指针变量p的指向的被分配的内存
    08     *in = NULL; //使得p指向NULL,也就是修改变量p的值
    09 }
    10 int main(int argc, char **argv)
    11 {
    12     char *p = NULL;
    13     p = strdup("hello_world");
    14     printf("   p = [%s], len = [%d]\n", p, strlen(p));
    15     printf("  &p = [%p], p\'s address\n", &p);
    16     printf("   p = [%p], p\'s value\n",  p);
    17     printf("  *p = [%c], value of addr No.[%p]\n", *p, p);
    18     foo(&p); //这里应该传入p的地址,即&p
    19  
    20     //感谢@ColoredCotton的贡献
    21     //而现在,这里的判断就会是false了
    22     if (NULL != p) { //这里的判断就有意义了
    23         free(p);
    24         p = NULL;
    25     }
    26     else {
    27         printf("p is NULL\n");
    28     }
    29     return 0;
    30 }

    运行一遍,看看打印如何:

    01 [michael@localhost mem-test]$ ./a.out
    02    p = [hello_world], len = [11]
    03   &p = [0xbf94014c], p's address
    04    p = [0x9964008], p's value
    05   *p = [h], value of addr No.[0x9964008]
    06  &in = [0xbf940130], in's address
    07   in = [0xbf94014c], in's value
    08  *in = [0x9964008]
    09 **in = [h]
    10 p is NULL
    11 [michael@localhost mem-test]$

    我想,根据打印信息来看,没什么需要解释的了。顺便还弄透彻了指针以及函数传参。


    (7)刚malloc后,马上就free,然后一直循环,会不会总是申请到同一块内存?

    答案:这不是真的。不信?你用这些代码测试一下就知道了:

    01 int main(int argc, char **argv)
    02 {
    03     char *p = NULL;
    04     int ra = 0;
    05     while(1) {
    06         ra = rand()%100+1; //生成一个1-100之间的随机数
    07         if (NULL != (p = (char*)malloc(ra))) {
    08             printf("p addr [%p], ra = [%d]\n", p, ra);
    09         }
    10         else {
    11             return -1;
    12         }
    13         free(p);
    14     }
    15     return 0;
    16 }

    看看打印吧:

    1 p addr [0x8bd8008], ra = [59]
    2 p addr [0x8bd8048], ra = [78]
    3 p addr [0x8bd8008], ra = [41]
    4 p addr [0x8bd8038], ra = [46]
    5 p addr [0x8bd8070], ra = [82]
    6 p addr [0x8bd8008], ra = [62]
    7 p addr [0x8bd8008], ra = [91]
    8 p addr [0x8bd8008], ra = [24]

    为什么不会一样呢?这个可以深究一下Linux系统的内存分配方式了,这就涉及到内核了。


    (8)malloc(0)返回的真的入man手册所说:要么是NULL,要么是一个unique的pointer?
    答案:不妨看下这段代码:

    01 int main(int argc, char **argv)
    02 {
    03     char *p = NULL;
    04     char *p0 = NULL;
    05     int ra = 0;
    06     while(1) {
    07         ra = rand()%100+1;
    08         if (NULL == (p = (char*)malloc(ra))) {
    09             printf("error occurs\n");
    10         }
    11         if (NULL != (p0 = malloc(0))) {
    12             printf("p0 addr [%p]\n", p0);
    13         }
    14         free(p);
    15         free(p0);
    16     }
    17     return 0;
    18 }

    打印如下所示:

    1 p0 addr [0x97eb008] #我随便截取了一段打印
    2 p0 addr [0x97eb008]
    3 p0 addr [0x97eb008]
    4 p0 addr [0x97eb040]
    5 p0 addr [0x97eb040]
    6 p0 addr [0x97eb040]

    所以从打印看来,我用Fedora14测试的时候,返回的既不是NULL,也不是一个唯一的地址,我现在也迷惑了,man手册中的unique到底应该如何理解。很遗憾man手册说得不太准确。如果你知道为什么,请告诉我,如果我哪一天弄明白了,我会在这里贴出来的。


    (9)如果你也和我一样,做了这么多的实验,你是不是发现,malloc得到的地址的值总是大于0x80000000的(32bits机器)?
    答案:不好意思,我也不知道为什么,做了好多次,不管如何重新编译、运行,得到的结果都是大于0x80000000的,如果你知道为什么,也请告诉我,如果我哪一天弄明白了,我会在这里贴出来的。

  • 相关阅读:
    failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected 排坑指南
    使用labelme制作自己的数据集
    linux安装anaconda3 conda: command not found
    windows端运行.sh脚本
    安装easydict
    tensorflow安装排坑笔记
    Dev怎么调试,怎么调试不了???
    NameError: name 'QApplication' is not defined 的解决办法
    绝对路径和相对路径
    关于Pycharm总是Indexing很久的问题
  • 原文地址:https://www.cnblogs.com/shihao/p/2876739.html
Copyright © 2020-2023  润新知