21天C语言代码训练营(第十八天)

作者: 天花板 | 来源:发表于2016-04-20 21:20 被阅读675次
    内存

    今天来聊聊一些常见的内存相关的问题。很多人认为,C语言程序设计中一个最难的部分就是和内存操作。因为它过于抽象,很难让初学者准确把握其特性。我今天在找配图的时候也很难把malloc出来的buffer和上面这张图片联系起来。这篇文章里,我们通过几个简单的题目帮助大家诠释C语言操作内存的相关问题。

    1. 栈空间不能外传

    前面的文章中我们讲过栈空间和堆空间的区别,它们有一个非常重要的区别是栈空间的使用有一个自动的生命周期,而堆空间需要程序员自己通过代码控制其生命周期。

    我们看一下下面这段代码:

    char* fun()
    {
        char a[60] = "ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHI";
        char b[60];
    
        strcpy(b, a);
        
        printf("%s\n", b);
    
        return b;
    }
    
    int main()
    {
        char* p = fun();
    
        printf("%s\n", p);
    
        return 0;
    }
    

    这段代码的执行结果如下:

    执行结果

    我们发现第二次打印的时候从fun()中传出的内存空间已经被修改过了,其实是被系统回收分配给其他变量了。

    char b[60]这句话在栈空间中申请了一个60个字符大小的空间,它的生命周期到函数结尾处。因此,当函数返回时,数组b的地址已经被系统回收了。

    因此,我们要记住以下两点:

    • 栈内地址不能传递到函数外
    • 站内地址不需要手动释放空间

    那如果我们一定要从函数内部传出一段有效内存怎么办呢?我们可以把代码做如下修改:

    char* fun()
    {
        char a[60] = "ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHI";
        char* p = (char*)malloc(60 * sizeof(char));
    
        strcpy(p, a);
        
        printf("%s\n", p);
    
        return p;
    }
    

    在函数中申请一段堆空间,这样就可以在函数退出后依然有效。但main函数中需要记着自行是否这段内存空间。

    int main()
    {
        char* p = fun();
    
        printf("%s\n", p);
    
        free(p);
    
        return 0;
    }
    

    2. 内存访问越界

    一说到内存访问我们就会想到用下标或指针访问内存的某个位置时不能访问到申请大小之外去。但常常有人犯下面这个错误:

    int main()
    {
        char a[12] = "Hello World";
        char* p = (char*)malloc(15 * sizeof(char));
    
        memcpy(p, a, 5);
    
        printf("%s\n", p);
        
        return 0;
    }
    

    这段代码问题在哪儿呢?先看看执行结果:

    执行结果

    本来想打印出Hello这个词,却在后面出现了乱码。其实原因很简单,我们申请到内存之后并没有初始化,因此这段内存中充满了乱码。而在我们使用memcpy的时候并没有把字符串的结尾标识符拷贝到p指针的内存中。于是,在打印时,程序会一直向后找字符串结束符,这样不确定因素很多。主要有下面两个危险:

    • 如果在乱码中恰好有一个字符串结束符,那么会打印出乱码
    • 如果乱码中没有结束符,那么会一直找下去产生访问越界,出现不可预测的错误

    那么正确的做法是什么呢?

    • 方法一:内存初始化

    在申请到内存之后立刻用0将它初始化,代码如下:

    int main()
    {
        char a[12] = "Hello World"; 
        char* p = (char*)malloc(15 * sizeof(char));
    
        memset(p, 0, 15 * sizeof(char));
        memcpy(p, a, 5);
    
        printf("%s\n", p);
        
        return 0;
    }
    
    • 方法二:拷贝之后加入字符串结束符

    在进行内存拷贝之后,人为加入字符串结束符,代码如下:

    int main()
    {
        char a[12] = "Hello World"; 
        char* p = (char*)malloc(15 * sizeof(char));
    
        memcpy(p, a, 5);
    
        p[5] = 0;
    
        printf("%s\n", p);
        
        return 0;
    }
    

    3. 内存释放顺序

    经常有人在释放结构体和成员变量指针时出现因为顺序错误而产生的内存泄露问题。这些问题我们在前面的项目中涉及过。先看一下这段代码:

    typedef struct _tagNode
    {
        int n;
        char* p;
    }Node;
    
    int main()
    {
        Node* pNode = (Node*)malloc(sizeof(Node));
        
        pNode->n = 1;
        pNode->p = (char*)malloc(10 * sizeof(char));
    
        strcpy(pNode->p, "Hello");
    
        printf("%s\n", pNode->p);
    
        free(pNode);
        free(pNode->p);
    
        return 0;
    }
    

    这段代码执行后会出现内存泄露,看出是什么原因了吗?

    程序中共有两个指针,pNode和p。当我们执行free(pNode)这句话时,保存p指针变量的空间已经被释放,因此在执行free(pNode->p)这句话时就会报错。正确的做法应该是这样:

    free(pNode->p);
    free(pNode);
    pNode = NULL;
    

    在释放内存空间时,我们有一个常用的方法:先申请的后释放。

    我是天花板,让我们一起在软件开发中自我迭代。
    如有任何问题,欢迎与我联系。


    上一篇:21天C语言代码训练营(第十七天)
    下一篇:21天C语言代码训练营(第十九天)

    相关文章

      网友评论

        本文标题:21天C语言代码训练营(第十八天)

        本文链接:https://www.haomeiwen.com/subject/hmftrttx.html