c語言執行時錯誤怎麼解決(c語言執行錯誤原因)

一、沒有為指標分配記憶體

定義了指標變數,但是沒有為指標分配記憶體,即指標沒有指向一塊合法的記憶體。淺顯的例子就不舉了,這裡舉幾個比較隱蔽的例子。

1、結構體成員指標未初始化

struct student

{

char *name;

int score;

}stu,*pstu;

int main()

{

strcpy(stu.name,"Jimy");

stu.score = 99;

return 0;

}

很多初學者犯了這個錯誤還不知道是怎麼回事。這裡定義了結構體變數stu,但是他沒想到這個結構體內部char *name 這成員在定義結構體變數stu 時,只是給name 這個指標變數本身分配了4 個位元組。name 指標並沒有指向一個合法的地址,這時候其內部存的只是一些亂碼。所以在呼叫strcpy 函式時,會將字串"Jimy"往亂碼所指的記憶體上拷貝,而這塊記憶體name 指標根本就無權訪問,導致出錯。解決的辦法是為name 指標malloc 一塊空間。

同樣,也有人犯如下錯誤:

int main()

{

pstu = (struct student*)malloc(sizeof(struct student));

strcpy(pstu->name,"Jimy");

pstu->score = 99;

free(pstu);

return 0;

}

為指標變數pstu 分配了記憶體,但是同樣沒有給name 指標分配記憶體。錯誤與上面第一種情況一樣,解決的辦法也一樣。這裡用了一個malloc 給人一種錯覺,以為也給name 指標分配了記憶體。

2、沒有為結構體指標分配足夠的記憶體

int main()

{

pstu = (struct student*)malloc(sizeof(struct student*));

strcpy(pstu->name,"Jimy");

pstu->score = 99;

free(pstu);

return 0;

}

為pstu 分配記憶體的時候,分配的記憶體大小不合適。這裡把sizeof(struct student)誤寫為sizeof(struct student*)。當然name 指標同樣沒有被分配記憶體。解決辦法同上。

3、函式的入口校驗

不管什麼時候,我們使用指標之前一定要確保指標是有效的。

一般在函式入口處使用assert(NULL != p)對引數進行校驗。在非引數的地方使用if(NULL != p)來校驗。但這都有一個要求,即p 在定義的同時被初始化為NULL 了。比如上面的例子,即使用if(NULL != p)校驗也起不了作用,因為name 指標並沒有被初始化為NULL,其內部是一個非NULL 的亂碼。

assert 是一個巨集,而不是函式,包含在assert.h 標頭檔案中。如果其後面括號裡的值為假,則程式終止執行,並提示出錯;如果後面括號裡的值為真,則繼續執行後面的程式碼。這個巨集只在Debug 版本上起作用,而在Release 版本被編譯器完全優化掉,這樣就不會影響程式碼的效能。

有人也許會問,既然在Release 版本被編譯器完全優化掉,那Release 版本是不是就完全沒有這個引數入口校驗了呢?這樣的話那不就跟不使用它效果一樣嗎?

是的,使用assert 巨集的地方在Release 版本里面確實沒有了這些校驗。但是我們要知道,assert 巨集只是幫助我們除錯程式碼用的,它的一切作用就是讓我們儘可能的在除錯函式的時候把錯誤排除掉,而不是等到Release 之後。它本身並沒有除錯功能。再有一點就是,引數出現錯誤並非本函式有問題,而是呼叫者傳過來的實參有問題。assert 巨集可以幫助我們定位錯誤,而不是排除錯誤。

二、為指標分配的記憶體太小

為指標分配了記憶體,但是記憶體大小不夠,導致出現越界錯誤。

char *p1 = “abcdefg”;

char *p2 = (char *)malloc(sizeof(char)*strlen(p1));

strcpy(p2,p1);

p1 是字串常量,其長度為7 個字元,但其所佔記憶體大小為8 個byte。初學者往往忘了字串常量的結束標誌“/0”。這樣的話將導致p1 字串中最後一個空字元“/0”沒有被拷貝到p2 中。解決的辦法是加上這個字串結束標誌符:

char *p2 = (char *)malloc(sizeof(char)*strlen(p1) 1*sizeof(char));

這裡需要注意的是,只有字串常量才有結束標誌符。比如下面這種寫法就沒有結束標誌符了:

char a[7] = {‘a’,’b’,’c’,’d’,’e’,’f’,’g’};

另外,不要因為char 型別大小為1 個byte 就省略sizof(char)這種寫法。這樣只會使你的程式碼可移植性下降。

三、 記憶體越界

記憶體分配成功,且已經初始化,但是操作越過了記憶體的邊界。這種錯誤經常是由於運算元組或指標時出現“多1”或“少1”。比如:

int a[10] = {0};

for (i=0; i<=10; i )

{

a[i] = i;

}

所以,for 迴圈的迴圈變數一定要使用半開半閉的區間,而且如果不是特殊情況,迴圈變數儘量從0 開始。

四、 給指標分配記憶體後,沒有初始化記憶體

犯這個錯誤往往是由於沒有初始化的概念或者是以為記憶體分配好之後其值自然為0。未初始化指標變數也許看起來不那麼嚴重,但是它確確實實是個非常嚴重的問題,而且往往出現這種錯誤很難找到原因。

曾經有一個學生在寫一個windows 程式時,想呼叫字型檔的某個字型。而呼叫這個字型檔需要填充一個結構體。他很自然的定義了一個結構體變數,然後把他想要的字型檔程式碼賦值給了相關的變數。但是,問題就來了,不管怎麼除錯,他所需要的這種字型效果總是不出來。我在檢查了他的程式碼之後,沒有發現什麼問題,於是單步除錯。在觀察這個結構體變數的記憶體時,發現有幾個成員的值為亂碼。就是其中某一個亂碼惹得禍!因為系統會按照這個結構體中的某些特定成員的值去字型檔中尋找匹配的字型,當這些值與字型檔中某種字型的某些項匹配時,就呼叫這種字型。但是很不幸,正是因為這幾個亂碼,導致沒有找到相匹配的字型!因為系統並無法區分什麼資料是亂碼,什麼資料是有效的資料。只要有資料,系統就理所當然的認為它是有效的。

也許這種嚴重的問題並不多見,但是也絕不能掉以輕心。所以在定義一個變數時,第一件事就是初始化。你可以把它初始化為一個有效的值,比如:

int i = 10;

char *p = (char *)malloc(sizeof(char));

但是往往這個時候我們還不確定這個變數的初值,這樣的話可以初始化為0 或NULL。

int i = 0;

char *p = NULL;

如果定義的是陣列的話,可以這樣初始化:

int a[10] = {0};

或者用memset 函式來初始化為0:

memset(a,0,sizeof(a));

memset 函式有三個引數,第一個是要被設定的記憶體起始地址;第二個引數是要被設定的值;第三個引數是要被設定的記憶體大小,單位為byte。這裡並不想過多的討論memset 函式的用法,如果想了解更多,請參考相關資料。

至於指標變數如果未被初始化,會導致if 語句或assert 巨集校驗失敗。

五、沒有釋放申請的記憶體,導致記憶體洩露

記憶體洩漏幾乎是很難避免的,不管是老手還是新手,都存在這個問題。甚至包括windows,Linux 這類軟體,都或多或少有記憶體洩漏。也許對於一般的應用軟體來說,這個問題似乎不是那麼突出,重啟一下也不會造成太大損失。但是如果你開發的是嵌入式系統軟體呢?比如汽車制動系統,心臟起搏器等對安全要求非常高的系統。你總不能讓心臟起搏器重啟吧,人家閻王老爺是非常好客的。

會產生洩漏的記憶體就是堆上的記憶體(這裡不討論資源或控制代碼等洩漏情況),也就是說由malloc 系列函式或new 操作符分配的記憶體。如果用完之後沒有及時free 或delete,這塊記憶體就無法釋放,直到整個程式終止。

1、告老還鄉求良田

怎麼去理解這個記憶體分配和釋放過程呢?先看下面這段對話:

萬歲爺:愛卿,你為朕立下了汗馬功勞,想要何賞賜啊?

某功臣:萬歲,黃金白銀,臣視之如糞土。臣年歲已老,欲告老還鄉。臣乞良田千畝以蔭後世,別無他求。

萬歲爺:愛卿,你勞苦功高,卻僅要如此小賞,朕今天就如你所願。戶部劉侍郎,檢視湖廣一帶是否還有千畝上等良田未曾封賞。

劉侍郎:長沙尚有五萬餘畝上等良田未曾封賞。

萬歲爺:在長沙撥良田千畝封賞愛卿。愛卿,良田千畝,你欲何用啊?

某功臣:謝萬歲。長沙一帶,適合種水稻,臣想用來種水稻。種水稻需要把田分為一畝一塊,方便耕種。

。。。。

2、如何使用malloc 函式

不要莫名其妙,其實上面這段小小的對話,就是malloc 的使用過程。malloc 是一個函式,專門用來從堆上分配記憶體。使用malloc 函式需要幾個要求:

記憶體分配給誰?這裡是把良田分配給某功臣。

分配多大記憶體?這裡是分配一千畝。

是否還有足夠記憶體分配?這裡是還有足夠良田分配。

記憶體的將用來儲存什麼格式的資料,即記憶體用來做什麼?

這裡是用來種水稻,需要把田分成一畝一塊。分配好的記憶體在哪裡?這裡是在長沙。

如果這五點都確定,那記憶體就能分配。下面先看malloc 函式的原型: (void *)malloc(int size) malloc 函式的返回值是一個void 型別的指標,引數為int 型別資料,即申請分配的記憶體大小,單位是byte。記憶體分配成功之後,malloc 函式返回這塊記憶體的首地址。你需要一個指標來接收這個地址。但是由於函式的返回值是void *型別的,所以必須強制轉換成你所接收的型別。也就是說,這塊記憶體將要用來儲存什麼型別的資料。比如: char *p = (char *)malloc(100); 在堆上分配了100 個位元組記憶體,返回這塊記憶體的首地址,把地址強制轉換成char *型別後賦給char *型別的指標變數p。同時告訴我們這塊記憶體將用來儲存char 型別的資料。也就是說你只能通過指標變數p 來操作這塊記憶體。這塊記憶體本身並沒有名字,對它的訪問是匿名訪問。

上面就是使用malloc 函式成功分配一塊記憶體的過程。但是,每次你都能分配成功嗎?

不一定。上面的對話,皇帝讓戶部侍郎查詢是否還有足夠的良田未被分配出去。使用malloc函式同樣要注意這點:如果所申請的記憶體塊大於目前堆上剩餘記憶體塊(整塊),則記憶體分配會失敗,函式返回NULL。注意這裡說的“堆上剩餘記憶體塊”不是所有剩餘記憶體塊之和,因為malloc 函式申請的是連續的一塊記憶體。

既然malloc 函式申請記憶體有不成功的可能,那我們在使用指向這塊記憶體的指標時,必須用if(NULL != p)語句來驗證記憶體確實分配成功了。

3、用malloc 函式申請0 位元組記憶體

另外還有一個問題:用malloc 函式申請0 位元組記憶體會返回NULL 指標嗎?

可以測試一下,也可以去查詢關於malloc 函式的說明文件。申請0 位元組記憶體,函式並不返回NULL,而是返回一個正常的記憶體地址。但是你卻無法使用這塊大小為0 的記憶體。這好尺子上的某個刻度,刻度本身並沒有長度,只有某兩個刻度一起才能量出長度。對於這一點一定要小心,因為這時候if(NULL != p)語句校驗將不起作用。

4、記憶體釋放

既然有分配,那就必須有釋放。不然的話,有限的記憶體總會用光,而沒有釋放的記憶體卻在空閒。與malloc 對應的就是free 函式了。free 函式只有一個引數,就是所要釋放的記憶體塊的首地址。比如上例: free(p); free 函式看上去挺狠的,但它到底作了什麼呢?其實它就做了一件事:斬斷指標變數與這塊記憶體的關係。比如上面的例子,我們可以說malloc 函式分配的記憶體塊是屬於p 的,因為我們對這塊記憶體的訪問都需要通過p 來進行。free 函式就是把這塊記憶體和p 之間的所有關係斬斷。從此p 和那塊記憶體之間再無瓜葛。至於指標變數p 本身儲存的地址並沒有改變,但是它對這個地址處的那塊記憶體卻已經沒有所有權了。那塊被釋放的記憶體裡面儲存的值也沒有改變,只是再也沒有辦法使用了。

這就是free 函式的功能。按照上面的分析,如果對p 連續兩次以上使用free 函式,肯定會發生錯誤。因為第一使用free 函式時,p 所屬的記憶體已經被釋放,第二次使用時已經無記憶體可釋放了。關於這點,我上課時讓學生記住的是:一定要一夫一妻制,不然肯定出錯。

malloc 兩次只free 一次會記憶體洩漏;malloc 一次free 兩次肯定會出錯。也就是說,在程式中malloc 的使用次數一定要和free 相等,否則必有錯誤。這種錯誤主要發生在迴圈使用malloc 函式時,往往把malloc 和free 次數弄錯了。這裡留個 練習:

寫兩個函式,一個生成連結串列,一個釋放連結串列。兩個函式的引數都只使用一個表頭指標。

5、記憶體釋放之後

既然使用free 函式之後指標變數p 本身儲存的地址並沒有改變,那我們就需要重新把p的值變為NULL: p = NULL; 這個NULL 就是我們前面所說的“栓野狗的鏈子”。如果你不栓起來遲早會出問題的。比如:在free(p)之後,你用if(NULL != p)這樣的校驗語句還能起作用嗎?例如:

char *p = (char *)malloc(100);

strcpy(p, “hello”);

free(p); /* p 所指的記憶體被釋放,但是p 所指的地址仍然不變*/

if (NULL != p)

{

/* 沒有起到防錯作用*/

strcpy(p, “world”); /* 出錯*/

}

釋放完塊記憶體之後,沒有把指標置NULL,這個指標就成為了“野指標”,也有書叫“懸垂指標”。這是很危險的,而且也是經常出錯的地方。所以一定要記住一條:free 完之後,一定要給指標置NULL。

同時留一個問題:對NULL 指標連續free 多次會出錯嗎?為什麼?如果讓你來設計free函式,你會怎麼處理這個問題?

六、記憶體已經被釋放了,但是繼續通過指標來使用

這裡一般有三種情況:

第一種:就是上面所說的,free(p)之後,繼續通過p 指標來訪問記憶體。解決的辦法就是給p 置NULL。

第二種:函式返回棧記憶體。這是初學者最容易犯的錯誤。比如在函式內部定義了一個陣列,卻用return 語句返回指向該陣列的指標。解決的辦法就是弄明白棧上變數的生命週期。

第三種:記憶體使用太複雜,弄不清到底哪塊記憶體被釋放,哪塊沒有被釋放。解決的辦法是重新設計程式,改善物件之間的呼叫關係。

上面詳細討論了常見的六種錯誤及解決對策,希望讀者仔細研讀,儘量使自己對每種錯誤發生的原因及預防手段爛熟於胸。一定要多練,多除錯程式碼,同時多總結經驗。

最後,如果你想更好的提升你的程式設計能力,好好學習C/C 程式設計知識的話!那麼你很幸運~

點選下方“瞭解更多”加入C語言/C 企鵝圈,這裡還有一些你可能不知道的趣事分享喲。