1. 程式人生 > >常見C語言記憶體錯誤

常見C語言記憶體錯誤

前言

C語言強大的原因之一在於幾乎能掌控所有的細節,包括對記憶體的處理,什麼時候使用記憶體,使用了多少記憶體,什麼時候該釋放記憶體,這都在程式設計師的掌控之中。而不像Java中,程式設計師是不需要花太多精力去處理垃圾回收的事情,因為有JVM在背後做著這一切。但是同樣地,能力越大,責任越大。不恰當地操作記憶體,經常會引起難以定位的災難性問題。今天我們就來看看有哪些常見的記憶體問題。

初始化堆疊中的資料

對申請的記憶體或自動變數進行初始化是一個好習慣,例如:

int test()
{
    int *a = (int*) malloc(10);
    /*判斷是否申請成功*/
    if(NULL == a)
    {
        return -1;
    }
    /*將其初始化為0*/
    memset(a,0,10);
    /*do something*/
    return 0;
}

我們經常需要在使用前將其初始化為0或使用calloc申請記憶體。關於初始化,在《C語言入坑指南-被遺忘的初始化》一文中,有更詳細的闡述。

緩衝區溢位

緩衝區溢位通常指的是向緩衝區寫入了超過緩衝區所能儲存的最大資料量的資料。同樣的,緩衝區溢位通常也伴隨著難以定位的問題。例如下面的程式碼就存在緩衝區溢位的可能:

/*bad code*/
#include <stdio.h>
#include <string.h>
int main(void)
{
    char buff[8] = {0};
    char *p = "0123456789";
    strcpy(buff,p);
    printf("%s\n",buff);
    return 0;
}

關於緩衝區溢位,可以通過《C語言入坑指南-緩衝區溢位》一文了解更多。

指標不等同於其指向的物件

我們可能常常錯誤性地認為指標物件的大小就是資料本身的大小,最常錯誤使用的就是下面的情況:

/*bad code*/
int test(int a[])
{
   size_t len = sizeof(a)/sizeof(int);
   /*do something*/
}

這裡計算陣列a的長度偶爾能夠如願,但實際上是錯誤的,因為陣列名作為引數時,是指向該陣列下標為0的元素的指標。因此sizeof(a)的值會是4或者8(取決於程式的位數)。

指標運算以指向物件大小為單位

對於下面的程式碼,ptr1 + 1之後,到底移動了多少個位元組?ptr2 + 1呢?

int arr[] = {1,2,3};
int *ptr1 = arr;
char *ptr2 = (char*)arr;

實際上,它們移動的位元組數,是以其指向物件大小為單位的。即ptr1 + 1會移動4位元組(int型別),而ptr2 + 1 會移動1位元組(char型別)。
下面的程式碼執行結果是什麼?

#include<stdio.h>
int main(void)
{

    int a[5] = {1,2,3,4,5};
    int *p = (int*)(&a+1);
    printf("%d,%d",*(a+1),*(p-1));
    return 0;
}

問題的答案也可在《C語言入坑指南-陣列之謎》中找到。

不可引用已釋放的記憶體

對於下面的程式碼:

/*bad code*/
char *getHelloString()
{
    char string[] = "hello";
    return string;
}

在其他地方呼叫getHelloString之後,如果再使用printf列印string,顯然是不可取的。因為在呼叫返回之後,string所指向的記憶體已經釋放了。有人可能會問了,為什麼返回int型別就可以使用呢?比如:

int getInt()
{
   int a = 10;
   return a;
}

呼叫getInt顯然能夠得到a的值,這是為什麼呢?因為你實際上返回的就是值10,而前面返回的是string的地址,這個值你也能獲取,但是要獲取這個地址值指向的記憶體,已經不可行了。

下面的情況也是應該避免的:

/*bad code*/
int *a = (int*)malloc(10);
/*do something*/
free(a);
a[0] = 10; //記憶體已經被釋放,不可再引用

在這個例子中可能很容易發現問題,但是在大型程式中,這樣的問題可能很難發現,一個建議就是在釋放a的記憶體後,顯式地將a置為NULL。即:

free(a);
a = NULL;

避免對NULL解引用

對於上面的例子,a置NULL之後還不夠,我們需要經常對入參進行檢查,避免對NULL解引用。這樣就避免引用已經釋放的記憶體。例如:

int calcSum(int *arr,int len)
{
    /*入慘檢查,避免引用空指標*/
    if(NULL == arr || 0 == len)
    {
        return -1;
    }
    /*do something*/
    return 0;
}

當然了,在C++中可以傳引用,而避免這種重複的檢查性程式碼。
下面的程式碼,同樣也是有問題的:

char *str = NULL;
printf("%s",str);

這裡str為NULL,卻將其作為字串列印,後果將是災難性的。

申請的記憶體不使用時需要釋放

使用malloc等申請的記憶體如果不使用free進行釋放,將會引起記憶體洩露。長期執行將會導致可用記憶體越來越少,程式也將會變得越來越卡頓。

/*bad code*/
int doSomething(void *data,size_t len)
{
    if(NULL == data)
    {
        return -1;
    }
    int *p = (int*)malloc(len);
    /*do something*/
    return 0; 
}

在這裡,doSomething中申請了記憶體卻沒有釋放,多次呼叫之後,將導致記憶體洩露。也就是說,malloc或calloc與free經常是成對出現的。

總結

如果控制不當,強大的同時,也會造成更多的危害。上面所列出的僅僅是一些比較常見的記憶體相關問題,總結如下:

  • 自動變數或申請的記憶體需要初始化
  • 避免緩衝區溢位
  • 指標不等同於指向的物件
  • 指標運算以指向大小為單位
  • 避免對NULL或已釋放的記憶體進行引用
  • 申請的記憶體不使用時及時釋放
  • 使用printf列印字串時避免使用空指標

你踩過哪些坑?歡迎留言評論。

思考

下面的程式碼有什麼問題?

int *arr = (int*)malloc(10);
/*do something*/
arr++;
free(arr);

公眾號【程式設計珠璣】:專注但不限於分享計算機程式設計基礎,Linux,C語言,C++,Python,資料庫等程式設計相關[原創]技術文章,號內包含大量經典電子書和視訊學習資源。歡迎一起交流學習,一起修煉計算機“內功”,知其然,更知其所以然。