美文网首页
C 语言常见的内存坑

C 语言常见的内存坑

作者: 要上班的斌哥 | 来源:发表于2017-08-29 23:18 被阅读145次

    C 语言常见的内存错误类型

    提起 C 语言,大家可能会第一个想到指针,接着可能会想起与内存有关的错误。我们就利用这篇博客来总结一下 C 语言中常见的与内存有关的错误!

    常见的内存错误
    1. 解引用坏指针
    2. 读取未初始化的内存
    3. 覆盖内存
    4. 解引用一个不存在的变量
    5. 多次释放一个内存 block
    6. 引用被释放的内存
    7. 内存 block 释放失败

    那么接下来,我们用具体的案例来说明一下上面提到的与内存有关的错误。

    Dereferencing Bad Pointers

    int val;
    scanf("%d",val);
    

    这个代码的本意是使用 scanf 从 stdin 读取一个整数到 val 这个变量。但是 scanf 需要的参数是一个格式字符串和变量的地址

    scanf("%d",&val);
    

    这个内存相关的错误是把 val 的值作为一个内存地址来使用了,程序会试图将一个整数写到这个内存位置上, 这样做会导致不可预知的后果,如果运气好的话,程序会由于 segmentation fault 导致崩溃。如果运气不好的话,val 的值对应到了虚拟内存中的一些有效的读写区域,导致 scanf 将数据覆盖到该内存区域,然后发生无法预料的错误,这种类型的错误难以被发现或者调试。

    Reading Uninitialized Memory

     /* return y = Ax */
    int *matvec(int **A, int *x) {
       int *y = (int *)malloc( N * sizeof(int) );
       int i, j;
       for (i=0; i<N; i++) {
          for (j=0; j<N; j++) {
             y[i] += A[i][j] * x[j];
          }
    }
    return y; }
    

    在 C 语言中,为初始化的全局变量总是会被加载器初始化为零,但是对于堆内存却不会主动初始化。
    在程序中,y[i] 这个变量的初始值并不一定等于零,但是从程序的写法来看,程序员默认 y[i] 这个变量的初始值就是为零。对于这个类型错误的解决办法就是正确的初始化变量,将 y[i] 设置为零。
    读取未初始化的内存错误,简单来说本来初始化的内存就都存在这个内存区域里面,但是程序到其他内存区域读写数据了。

    Overwriting Memory

    1、错误估计了对象的大小

     int **p;
    p = (int **)malloc( N * sizeof(int) );
    for (i=0; i<N; i++) {
       p[i] = (int *)malloc( M * sizeof(int) );
    }
    

    这个程序是创建一个由 N 个指针组成的数组,每个指针指向一个包含 M 个 int 类型的元素的数组。
    但是

    p = (int **)malloc( N * sizeof(int) ); 
    

    这句代码是假设了指针和指针它们指向的对象是大小相同的,将sizeof(int) 和 sizeof(int*) 混同,这段代码在 int 字节大小和指向 int 的指针的字节大小相同的机器上会正确运行,但是如果不相等就会出问题,正确的代码应该是

    p = (int **)malloc( N * sizeof( int * ) ); 
    

    2、错位错误

    int **p;
    p = (int **)malloc( N * sizeof(int *) );
    for (i=0; i<=N; i++) {
       p[i] = (int *)malloc( M * sizeof(int) );
    }
    

    这段代码是创建了一个 N 个元素的指针数组,但是在初始化指针数组的时候初始化了 N+1 个,把 p 数组后面的内存位置覆盖掉了。

    3、未做数组越界检查

     char s[8];
    int i;
    gets(s);  /* reads “123456789” from stdin */
    

    这个程序不检查输入的字符串的大小就写入栈中的目标缓冲区,那么就会造成缓冲区溢出错误,为了避免这个错误不要使用 gets 函数而是使用 fgets 函数,这个函数有对字符串的大小进行限制。

    4、误解指针运算

    int *search(int *p, int val) {
       while (p && *p != val)
          p += sizeof(int);
    return p; 
    }
    

    5、引用指针而不是它所指的对象

    int *getPacket(int **packets, int *size) {
       int *packet;
       packet = packets[0];
       packets[0] = packets[*size - 1];
       *size--;   // what is happening here?
       reorderPackets(packets, *size);
       return(packet);
    }
    

    这段代码是由于没有留意 C 操作符的优先级和结合性导致错误的操作了指针,而不是指针所指的对象。

    *size--
    

    本意是减少 size 指针指向的值,但是实际运行情况是减少的是指针自己的值。运气够好的话,程序马上出错。要是运气差的话,程序运行了一段时间后才出错,这个时候估计都不知道上哪里寻找错误了。
    程序中的 p 指针运算 p += sizeof(int) ,每次都把指针加了4,这样导致访问了数组中的每 4 个整数。指针的算术操作是以它们指向的对象的大小为单位来操作的,但是这个对象的大小单位不一定是 int 。

    6、引用不存在的变量

    int *foo () {
       int val;
       return &val;
    }
    

    这段程序主要是利用栈的理解,这个函数返回一个指针 &val,指向栈里的一个局部变量,然后弹出它的栈帧,这个时候虽然 &val 仍然指向一个合法的内存地址,但是它已经不再是指向 val 变量了。当 &val 指向的内存地址被重新利用之后,程序会带来令人困惑的运行结果。

    Freeing Blocks Multiple Times

     x = (int *)malloc( N * sizeof(int) );
            <manipulate x>
    free(x); ...
    y = (int *)malloc( M * sizeof(int) );
    free(x);
            <manipulate y>
    

    这段程序把指针 x 指向的内存地址释放了 2 次,第一次释放了正确的内存地址,第二次释放的时候有可能会将已经写在该内存地址的数据释放掉,从而造成无法预料的后果。

    Referencing Freed Blocks

     x = (int *)malloc( N * sizeof(int) );
      <manipulate x>
    free(x); ...
    y = (int *)malloc( M * sizeof(int) );
    for (i=0; i<M; i++)
       y[i] = x[i]++;
    

    这个程序的错误是引用了已经被释放了的堆块中的数据,这个类型的错误只会在程序执行的后面才会显示出破坏效果。

    Failing to Free Blocks (Memory Leaks)

    foo() {
       int *x = (int *)malloc(N*sizeof(int));
       ...
       return;
    }
    

    这段程序会引起内存泄漏,内存泄漏是缓慢的隐形杀手,在堆里分配了块,使用完之后忘记释放,那么就创建了垃圾。如果垃圾变多了,运气够差的话,会占用整个虚拟地址空间。像是一些比较重要的进程如守护进程,这个守护进程是不会终止的,所以内存泄漏对这类不会终止的进程来说,危害巨大。

    参考

    这笔记来自于学习华盛顿大学的 《软硬件接口》 课程的课程记录,

    相关文章

      网友评论

          本文标题:C 语言常见的内存坑

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