人人都寫過的五個Bug!
大家好,我是良許。
計算機專業(yè)的小伙伴,在學(xué)校期間一定學(xué)過 C 語言。它是眾多高級語言的鼻祖,深入學(xué)習(xí)這門語言會對計算機原理、操作系統(tǒng)、內(nèi)存管理等等底層相關(guān)的知識會有更深入的了解,所以我在直播的時候,多次強調(diào)大家一定要好好學(xué)習(xí)這門語言。
但是,即使是最有經(jīng)驗的程序員也會寫出各種各樣的 Bug。本文就盤點一下學(xué)習(xí)或使用 C 語言過程中,非常容易出現(xiàn)的 5 個 Bug,以及如何規(guī)避這些 Bug。
這篇文章主要面向初學(xué)者,老鳥可以忽略哈(其實不少老鳥依然還會犯這些低級錯誤哦)~
1. 變量未初始化
當(dāng)程序啟動時,系統(tǒng)會給它自動分配一塊內(nèi)存,程序可以用它來存儲數(shù)據(jù)。所以如果你在定義一個變量時,在未初始化的情況下,它的值有可能是任意的。
但這也不是絕對的,有些環(huán)境就會在程序啟動時自動將內(nèi)存「清零」,因此每個變量默認值都是零??紤]到可移植性,最好要將變量進行初始化,這是一名合格軟件工程師應(yīng)該養(yǎng)成的好習(xí)慣。
我們來看下下面這個使用幾個變量和兩個數(shù)組的示例程序:
- #include <stdio.h>
- #include <stdlib.h>
- int 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;
- }
這段程序沒有對變量進行初始化,所以變量的值有可能是隨機的,不一定是零。在我的電腦上它的運行結(jié)果如下 :
- 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
從結(jié)果可以看出,i 和 j 的值剛好是 0,但 k 值為 32766。在 numbers 數(shù)組中,大多數(shù)元素也恰好是零,除了第三個(4199024)。
在不同的操作系統(tǒng)上編譯這段相同的程序,運行的結(jié)果有可能又是不一樣的。所以千萬不要覺得你的結(jié)果就是正確唯一的,一定要考慮可移植性。
例如,這是在 FreeDOS 上運行的相同程序的結(jié)果:
- 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
可以看出來,運行的結(jié)果跟上面幾乎是天差地別。所以,對變量進行初始化將為你省去很多不必要的麻煩,也便于將來的調(diào)試。
2. 數(shù)組越界
在計算機世界里,都是從 0 開始計數(shù),但總有人有意無意忘記這點。比如一個數(shù)組長度為 10 ,想要獲取最后一個元素的值,總有人用 array[10] ……
別問,問就是我寫過……
新手朋友犯這種低級錯誤特別多。我們來看下數(shù)組越界會發(fā)生什么。
- #include <stdio.h>
- #include <stdlib.h>
- int 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;
- }
請注意,程序初始化了數(shù)組 numbers 所有元素的值(0~4),但是越界讀取了第 0~9 元素的值??梢钥闯鰜?,前五個值是正確的,但之后鬼都不知道這些值會是什么:
- 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
所以大家在寫代碼過程中,一定要知道數(shù)組的邊界。像這種數(shù)據(jù)讀取的還好,如果一旦對這些內(nèi)存進行寫操作,直接就 core dump !
3. 字符串溢出
在 C 編程語言中,字符串是一組 char 值,也可以將其視為數(shù)組。因此,你也需要避免超出字符串的范圍。如果超出,則稱為字符串溢出。
為了測試字符串溢出,一種簡單方法是使用 gets 函數(shù)讀取數(shù)據(jù)。gets 函數(shù)非常危險,因為它不知道接收它的字符串中可以存儲多少數(shù)據(jù),只會天真地從用戶那里讀取數(shù)據(jù)。
如果用戶輸入字符串比較短那很好,但如果用戶輸入的值超過接收字符串的長度,則可能是災(zāi)難性的。
下面我們來演示一下這個現(xiàn)象:
- #include <stdio.h>
- #include <string.h>
- int main()
- {
- char name[10]; /* Such as "Beijing" */
- 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("<%s> is length %d\n", name, strlen(name));
- printf("var1 = %d; var2 = %d\n", var1, var2);
- /* done */
- puts("Ok");
- return 0;
- }
在這段代碼里,接收數(shù)組的長度為 10 ,所以當(dāng)輸入數(shù)據(jù)長度小于 10 的話,程序運行就沒問題。
例如,輸入城市 Beijing ,長度為 7 :
- var1 = 1; var2 = 2
- Where do you live?
- Beijing
- <Beijing> is length 7
- var1 = 1; var2 = 2
- Ok
威爾士小鎮(zhèn) Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch 是世界上名字最長的城市,這個字符串有 58 個字符,遠遠超出了 name 變量中可保留的 10 個字符。
如果輸入這個字符串,其結(jié)果是程序運行內(nèi)存的其它位置,比如 var1和var2 ,都有可能被波及:
- var1 = 1; var2 = 2
- Where do you live?
- Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
- <Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch> is length 58
- var1 = 2036821625; var2 = 2003266668
- Ok
- Segmentation fault (core dumped)
在中止之前,程序使用長字符串覆蓋內(nèi)存的其他部分。請注意,var1 和 var2 不再是它們的起始值 1 和 2 。
所以我們需要使用更安全的方法來讀取用戶數(shù)據(jù)。例如,getline 函數(shù)就是一個不錯的選擇,它將分配足夠大的內(nèi)存來存儲用戶輸入,因此用戶不會因輸入太長字符串而意外溢出。
4. 內(nèi)存重復(fù)釋放
良好的 C 編程規(guī)則之一是,如果分配了內(nèi)存,就一定要將其釋放。
我們可以使用 malloc 函數(shù)為數(shù)組和字符串申請內(nèi)存,系統(tǒng)將開辟一塊內(nèi)存并返回一個指向該內(nèi)存起始地址的指針。內(nèi)存使用完畢后,我們一定要記得使用 free 函數(shù)釋放內(nèi)存,然后系統(tǒng)將該內(nèi)存標(biāo)記為未使用。
但是,這個過程中,你只能調(diào)用 free 函數(shù)一次。如果你第二次調(diào)用 free 函數(shù),將導(dǎo)致意外行為,而且可能會破壞你的程序。
下面我們舉個簡單的例子:
- #include <stdio.h>
- #include <stdlib.h>
- int 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");
- }
運行此程序會導(dǎo)致第二次調(diào)用 free 函數(shù)時出現(xiàn) core dump 錯誤:
- malloc an array ...
- malloc succeeded
- Free the array...
- Free the array...
- free(): double free detected in tcache 2
- Aborted (core dumped)
那么怎么避免多次調(diào)用 free 函數(shù)呢?一個最簡單的方法就是將 malloc 和 free 語句放在一個函數(shù)里。
如果你將 malloc 放在一個函數(shù)里,而將 free 放在另一個函數(shù)里,那么,在使用的過程中,如果邏輯設(shè)計不恰當(dāng),都有可能出現(xiàn) free 被調(diào)用多次的情況。
5. 使用無效的文件指針
文件是操作系統(tǒng)里一種非常常見的數(shù)據(jù)存儲方式。例如,您可以將程序的配置信息存儲在名為 config.dat 文件里,程序運行時,就可以調(diào)用這個文件,讀取配置信息。
因此,從文件中讀取數(shù)據(jù)的能力對所有程序員都很重要。但是,如果你要讀取的文件不存在怎么辦?
在 C 語言中,要讀取文件一般是先使用 fopen 函數(shù)打開文件,然后該函數(shù)返回指向文件的流指針。
如果您要讀取的文件不存在或您的程序無法讀取,則 fopen 函數(shù)將返回 NULL 。在這種情況下,我們?nèi)匀粚ζ溥M行操作,會發(fā)生什么情況?我們一起來看下:
- #include <stdio.h>
- int 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("<%c>", ch);
- }
- fclose(pfile);
- /* done */
- puts("Ok");
- return 0;
- }
當(dāng)你運行這個程序時,如果 FILE.TXT 這個文件不存在,那么 pfile 將返回 NULL。在這種情況下我們還對 pfile 進行寫操作的話,會立刻導(dǎo)致 core dump :
- Open the FILE.TXT file ...
- Now display the contents of FILE.TXT ...
- Segmentation fault (core dumped)
所以,我們要始終檢查文件指針是否有效。例如,在調(diào)用 fopen 函數(shù)打開文件后,使用 if (pfile != NULL) 以確保指針是可以使用的。
小結(jié)
再有經(jīng)驗的程序員都有可能犯錯誤,所以寫代碼的時候我們要嚴謹再嚴謹。但是,如果你養(yǎng)成一些良好的習(xí)慣,并添加一些額外的代碼來檢查這五種類型的錯誤,則可以避免嚴重的 C 編程錯誤。