c語言程式設計常見錯誤集錦(c語言常見錯誤總結)

你寫的程式老是提示:陣列越界?變數未初始化?字串溢位?那麼巧了!

即使是最好的程式設計師也無法完全避免錯誤。這些錯誤可能會引入安全漏洞、導致程式崩潰或產生意外操作,具體影響要取決於程式的執行邏輯。

C 語言有時名聲不太好,因為它不像近期的程式語言(比如 Rust)那樣具有記憶體安全性。但是通過額外的程式碼,一些最常見和嚴重的 C 語言錯誤是可以避免的。下文講解了可能影響應用程式的五個錯誤以及避免它們的方法:

1、未初始化的變數

程式啟動時,系統會為其分配一塊記憶體以供儲存資料。這意味著程式啟動時,變數將獲得記憶體中的一個隨機值。

有些程式設計環境會在程式啟動時特意將記憶體“清零”,因此每個變數都得有初始的零值。程式中的變數都以零值作為初始值,聽上去是很不錯的。但是在 C 程式設計規範中,系統並不會出現初始化變數。

看一下這個使用了若干變數和兩個陣列的示例程式:

#include#includeint main() {   int i, j, k;   int numbers[5];   int *array;   puts("These variables are not initialized:");   printf("  i = %d/n", i);   printf("  j = %d/n", j);   printf("  k = %d/n", k);   puts("This array is not initialized:");   for (i = 0; i < 5; i  ) {     printf("  numbers[%d] = %d/n", i, numbers[i]);   }   puts("malloc an array ...");   array = malloc(sizeof(int) * 5);   if (array) {     puts("This malloc'ed array is not initialized:");     for (i = 0; i < 5; i  ) {       printf("  array[%d] = %d/n", i, array[i]);     }     free(array);   }   /* done */   puts("Ok");   return 0; }

這個程式不會初始化變數,所以變數以系統記憶體中的隨機值作為初始值。在我的 Linux 系統上編譯和執行這個程式,會看到一些變數恰巧有“零”值,但其他變數並沒有:

These variables are not initialized:   i = 0   j = 0   k = 32766 This array is not initialized:   numbers[0] = 0   numbers[1] = 0   numbers[2] = 4199024   numbers[3] = 0   numbers[4] = 0 malloc an array ... This malloc'ed array is not initialized:   array[0] = 0   array[1] = 0   array[2] = 0   array[3] = 0   array[4] = 0 Ok

很幸運,i和j變數是從零值開始的,但k的起始值為 32766。在numbers陣列中,大多數元素也恰好從零值開始,只有第三個元素的初始值為 4199024。

在不同的系統上編譯相同的程式,可以進一步顯示未初始化變數的危險性。不要誤以為“全世界都在執行 Linux”,你的程式很可能某天在其他平臺上執行。例如,下面是在 FreeDOS 上執行相同程式的結果:

These variables are not initialized:   i = 0   j = 1074   k = 3120 This array is not initialized:   numbers[0] = 3106   numbers[1] = 1224   numbers[2] = 784   numbers[3] = 2926   numbers[4] = 1224 malloc an array ... This malloc'ed array is not initialized:   array[0] = 3136   array[1] = 3136   array[2] = 14499   array[3] = -5886   array[4] = 219 Ok

永遠都要記得初始化程式的變數。如果你想讓變數將以零值作為初始值,請額外新增程式碼將零分配給該變數。預先編好這些額外的程式碼,這會有助於減少日後讓人頭疼的除錯過程。

2、陣列越界

C 語言中,陣列索引從零開始。這意味著對於長度為 10 的陣列,索引是從 0 到 9;長度為 1000 的陣列,索引則是從 0 到 999。

程式設計師有時會忘記這一點,他們從索引 1 開始引用陣列,產生了“大小差一”off by one錯誤。在長度為 5 陣列中,程式設計師在索引“5”處使用的值,實際上並不是陣列的第 5 個元素。相反,它是記憶體中的一些其他值,根本與此陣列無關。

這是一個陣列越界的示例程式。該程式使用了一個只含有 5 個元素的陣列,但卻引用了該範圍之外的陣列元素:

#include#includeint main() {   int i;   int numbers[5];   int *array;   /* test 1 */   puts("This array has five elements (0 to 4)");   /* initalize the array */   for (i = 0; i < 5; i  ) {     numbers[i] = i;   }   /* oops, this goes beyond the array bounds: */   for (i = 0; i < 10; i  ) {     printf("  numbers[%d] = %d/n", i, numbers[i]);   }   /* test 2 */   puts("malloc an array ...");   array = malloc(sizeof(int) * 5);   if (array) {     puts("This malloc'ed array also has five elements (0 to 4)");     /* initalize the array */     for (i = 0; i < 5; i  ) {       array[i] = i;     }     /* oops, this goes beyond the array bounds: */     for (i = 0; i < 10; i  ) {       printf("  array[%d] = %d/n", i, array[i]);     }     free(array);   }   /* done */   puts("Ok");   return 0; }

可以看到,程式初始化了陣列的所有值(從索引 0 到 4),然後重新索引 0 開始讀取,結尾是索引 9 而不是索引 4。前五個值是正確的,再後面的值會讓你不知所以:

This array has five elements (0 to 4)   numbers[0] = 0   numbers[1] = 1   numbers[2] = 2   numbers[3] = 3   numbers[4] = 4   numbers[5] = 0   numbers[6] = 4198512   numbers[7] = 0   numbers[8] = 1326609712   numbers[9] = 32764 malloc an array ... This malloc'ed array also has five elements (0 to 4)   array[0] = 0   array[1] = 1   array[2] = 2   array[3] = 3   array[4] = 4   array[5] = 0   array[6] = 133441   array[7] = 0   array[8] = 0   array[9] = 0 Ok

引用陣列時,始終要記得追蹤陣列大小。將陣列大小儲存在變數中;不要對陣列大小進行硬編碼hard-code。否則,如果後期該識別符號指向另一個不同大小的陣列,卻忘記更改硬編碼的陣列長度時,程式就可能會發生陣列越界。

3、字串溢位

字串只是特定型別的陣列。在 C 語言中,字串是一個由char型別值組成的陣列,其中用一個零字元表示字串的結尾。

因此,與陣列一樣,要注意避免超出字串的範圍。有時也稱之為字串溢位

使用gets函式讀取資料是一種很容易發生字串溢位的行為方式。gets函式非常危險,因為它不知道在一個字串中可以儲存多少資料,只會機械地從使用者那裡讀取資料。如果使用者輸入像foo這樣的短字串,不會發生意外;但是當使用者輸入的值超過字串長度時,後果可能是災難性的。

下面是一個使用gets函式讀取城市名稱的示例程式。在這個程式中,我還新增了一些未使用的變數,來展示字串溢位對其他資料的影響:

#include#includeint main() {   char name[10];                       /* Such as "Chicago" */   int var1 = 1, var2 = 2;   /* show initial values */   printf("var1 = %d; var2 = %d/n", var1, var2);   /* this is bad .. please don't use gets */   puts("Where do you live?");   gets(name);   /* show ending values */   printf(" is length %d/n", name, strlen(name));   printf("var1 = %d; var2 = %d/n", var1, var2);   /* done */   puts("Ok");   return 0; }

當你測試類似的短城市名稱時,該程式執行良好,例如伊利諾伊州的 Chicago 或北卡羅來納州的Raleigh:

var1 = 1; var2 = 2 Where do you live? Raleighis length 7 var1 = 1; var2 = 2 Ok

威爾士的小鎮
   Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch 有著世界上最長的名字之一。這個字串有 58 個字元,遠遠超出了 name 變數中保留的 10 個字元。結果,程式將值儲存在記憶體的其他區域,覆蓋了 var1 和 var2 的值:

var1 = 1; var2 = 2 Where do you live? Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogochis length 58 var1 = 2036821625; var2 = 2003266668 Ok Segmentation fault (core dumped)

在執行結束之前,程式會用長字串覆蓋記憶體的其他部分割槽域。注意,var1和var2的值不再是起始的1和2。

避免使用gets函式,改用更安全的方法來讀取使用者資料。例如,getline函式會分配足夠的記憶體來儲存使用者輸入,因此不會因輸入長值而發生意外的字串溢位。

4、重複釋放記憶體

“分配的記憶體要手動釋放”是良好的 C 語言程式設計原則之一。程式可以使用malloc函式為陣列和字串分配記憶體,該函式會開闢一塊記憶體,並返回一個指向記憶體中起始地址的指標。之後,程式可以使用free函式釋放記憶體,該函式會使用指標將記憶體標記為未使用。

但是,你應該只使用一次free函式。第二次呼叫free會導致意外的後果,可能會毀掉你的程式。下面是一個針對此點的簡短示例程式。程式分配了記憶體,然後立即釋放了它。但為了模仿一個健忘但有條理的程式設計師,我在程式結束時又一次釋放了記憶體,導致兩次釋放了相同的記憶體:

#include#includeint main() {   int *array;   puts("malloc an array ...");   array = malloc(sizeof(int) * 5);   if (array) {     puts("malloc succeeded");     puts("Free the array...");     free(array);   }   puts("Free the array...");   free(array);   puts("Ok"); }

執行這個程式會導致第二次使用 free 函式時出現戲劇性的失敗:

malloc an array ... malloc succeeded Free the array... Free the array... free(): double free detected in tcache 2 Aborted (core dumped)

要記得避免在陣列或字串上多次呼叫free。將malloc和free函式定位在同一個函式中,這是避免重複釋放記憶體的一種方法。

例如,一個紙牌遊戲程式可能會在主函式中為一副牌分配記憶體,然後在其他函式中使用這副牌來玩遊戲。記得在主函式,而不是其他函式中釋放記憶體。將malloc和free語句放在一起有助於避免多次釋放記憶體。

5、使用無效的檔案指標

檔案是一種便捷的資料儲存方式。例如,你可以將程式的配置資料儲存在config.dat檔案中。Bash shell 會從使用者家目錄中的.bash_profile讀取初始化指令碼。GNU Emacs 編輯器會尋找檔案.emacs以從中確定起始值。而 Zoom 會議客戶端使用zoomus.conf檔案讀取其程式配置。

所以,從檔案中讀取資料的能力幾乎對所有程式都很重要。但是假如要讀取的檔案不存在,會發生什麼呢?

在 C 語言中讀取檔案,首先要用fopen函式開啟檔案,該函式會返回指向檔案的流指標。你可以結合其他函式,使用這個指標來讀取資料,例如fgetc會逐個字元地讀取檔案。

如果要讀取的檔案不存在或程式沒有讀取許可權,fopen函式會返回NULL作為檔案指標,這表示檔案指標無效。但是這裡有一個示例程式,它機械地直接去讀取檔案,不檢查fopen是否返回了NULL:

#includeint main() {   FILE *pfile;   int ch;   puts("Open the FILE.TXT file ...");   pfile = fopen("FILE.TXT", "r");   /* you should check if the file pointer is valid, but we skipped that */   puts("Now display the contents of FILE.TXT ...");   while ((ch = fgetc(pfile)) != EOF) {     printf("", ch);   }   fclose(pfile);   /* done */   puts("Ok");   return 0; }

當你執行這個程式時,第一次呼叫 fgetc 會失敗,程式會立即終止:

Open the FILE.TXT file ... Now display the contents of FILE.TXT ... Segmentation fault (core dumped)

始終檢查檔案指標以確保其有效。例如,在呼叫fopen開啟一個檔案後,用類似if (pfile != NULL)的語句檢查指標,以確保指標是可以使用的。

人都會犯錯,最優秀的程式設計師也會產生程式設計錯誤。但是,遵循上面這些準則,新增一些額外的程式碼來檢查這五種型別的錯誤,就可以避免最嚴重的 C 語言程式設計錯誤。提前編寫幾行程式碼來捕獲這些錯誤,可能會幫你節省數小時的除錯時間。

via:https://opensource.com/article/21/10/programming-bugs

作者:Jim Hall,本文由LCTT原創編譯,Linux中國榮譽推出

寫在最後:對於準備學習C/C 程式設計的小夥伴,如果你想更好的提升你的程式設計核心能力(內功)不妨從現在開始!

程式設計學習書籍分享:

程式設計學習視訊分享:

整理分享(多年學習的原始碼、專案實戰視訊、專案筆記,基礎入門教程)

歡迎轉行和學習程式設計的夥伴,利用更多的資料學習成長比自己琢磨更快哦!

對於C/C 感興趣可以關注小編在後臺私信我:【程式設計交流】一起來學習哦!可以領取一些C/C 的專案學習視訊資料哦!已經設定好了關鍵詞自動回覆,自動領取就好了!