人人都寫過的5個Bug!
時間:2021-11-04 16:12:19
手機(jī)看文章
掃描二維碼
隨時隨地手機(jī)看文章
[導(dǎo)讀]計算機(jī)專業(yè)的小伙伴,在學(xué)校期間一定學(xué)過C語言。它是眾多高級語言的鼻祖,深入學(xué)習(xí)這門語言會對計算機(jī)原理、操作系統(tǒng)、內(nèi)存管理等等底層相關(guān)的知識會有更深入的了解,所以我在直播的時候,多次強(qiáng)調(diào)大家一定要好好學(xué)習(xí)這門語言。但是,即使是最有經(jīng)驗的程序員也會寫出各種各樣的Bug。本文就盤點一下...
計算機(jī)專業(yè)的小伙伴,在學(xué)校期間一定學(xué)過 C 語言。它是眾多高級語言的鼻祖,深入學(xué)習(xí)這門語言會對計算機(jī)原理、操作系統(tǒng)、內(nèi)存管理等等底層相關(guān)的知識會有更深入的了解,所以我在直播的時候,多次強(qiáng)調(diào)大家一定要好好學(xué)習(xí)這門語言。
但是,即使是最有經(jīng)驗的程序員也會寫出各種各樣的 Bug。本文就盤點一下學(xué)習(xí)或使用 C 語言過程中,非常容易出現(xiàn)的 5 個 Bug,以及如何規(guī)避這些 Bug。這篇文章主要面向初學(xué)者,老鳥可以忽略哈(其實不少老鳥依然還會犯這些低級錯誤哦)~
#include?
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;
}
這段程序沒有對變量進(jìn)行初始化,所以變量的值有可能是隨機(jī)的,不一定是零。在我的電腦上它的運(yùn)行結(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?=?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
可以看出來,運(yùn)行的結(jié)果跟上面幾乎是天差地別。所以,對變量進(jìn)行初始化將為你省去很多不必要的麻煩,也便于將來的調(diào)試。
#include?
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)存進(jìn)行寫操作,直接就 core dump !
#include?
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 的話,程序運(yùn)行就沒問題。例如,輸入城市 Beijing ,長度為 7 :var1?=?1;?var2?=?2
Where?do?you?live?
Beijing
?is?length?7
var1?=?1;?var2?=?2
Ok
威爾士小鎮(zhèn)
Where?do?you?live?
Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
?is?length?58
var1?=?2036821625;?var2?=?2003266668
Ok
Segmentation?fault?(core?dumped)
在中止之前,程序使用長字符串覆蓋內(nèi)存的其他部分。請注意,
#include?
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");
}
運(yùn)行此程序會導(dǎo)致第二次調(diào)用
malloc?succeeded
Free?the?array...
Free?the?array...
free():?double?free?detected?in?tcache?2
Aborted?(core?dumped)
那么怎么避免多次調(diào)用
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)你運(yùn)行這個程序時,如果 FILE.TXT 這個文件不存在,那么 pfile 將返回 NULL。在這種情況下我們還對 pfile 進(jìn)行寫操作的話,會立刻導(dǎo)致 core dump :Open?the?FILE.TXT?file?...
Now?display?the?contents?of?FILE.TXT?...
Segmentation?fault?(core?dumped)
所以,我們要始終檢查文件指針是否有效。例如,在調(diào)用
但是,即使是最有經(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)存「清零」,因此每個變量默認(rèn)值都是零。考慮到可移植性,最好要將變量進(jìn)行初始化,這是一名合格軟件工程師應(yīng)該養(yǎng)成的好習(xí)慣。我們來看下下面這個使用幾個變量和兩個數(shù)組的示例程序:#include?#include?
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;
}
這段程序沒有對變量進(jìn)行初始化,所以變量的值有可能是隨機(jī)的,不一定是零。在我的電腦上它的運(yùn)行結(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)上編譯這段相同的程序,運(yùn)行的結(jié)果有可能又是不一樣的。所以千萬不要覺得你的結(jié)果就是正確唯一的,一定要考慮可移植性。例如,這是在 FreeDOS 上運(yùn)行的相同程序的結(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
可以看出來,運(yùn)行的結(jié)果跟上面幾乎是天差地別。所以,對變量進(jìn)行初始化將為你省去很多不必要的麻煩,也便于將來的調(diào)試。
2. 數(shù)組越界
在計算機(jī)世界里,都是從 0 開始計數(shù),但總有人有意無意忘記這點。比如一個數(shù)組長度為 10 ,想要獲取最后一個元素的值,總有人用 array[10] ……別問,問就是我寫過……新手朋友犯這種低級錯誤特別多。我們來看下數(shù)組越界會發(fā)生什么。#include?#include?
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)存進(jìn)行寫操作,直接就 core dump !
3. 字符串溢出
在 C 編程語言中,字符串是一組char
值,也可以將其視為數(shù)組。因此,你也需要避免超出字符串的范圍。如果超出,則稱為字符串溢出。為了測試字符串溢出,一種簡單方法是使用 gets
函數(shù)讀取數(shù)據(jù)。gets
函數(shù)非常危險,因為它不知道接收它的字符串中可以存儲多少數(shù)據(jù),只會天真地從用戶那里讀取數(shù)據(jù)。如果用戶輸入字符串比較短那很好,但如果用戶輸入的值超過接收字符串的長度,則可能是災(zāi)難性的。下面我們來演示一下這個現(xiàn)象:#include?#include?
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 的話,程序運(yùn)行就沒問題。例如,輸入城市 Beijing ,長度為 7 :var1?=?1;?var2?=?2
Where?do?you?live?
Beijing
var1?=?1;?var2?=?2
Ok
Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
是世界上名字最長的城市,這個字符串有 58 個字符,遠(yuǎn)遠(yuǎn)超出了 name
變量中可保留的 10 個字符。如果輸入這個字符串,其結(jié)果是程序運(yùn)行內(nèi)存的其它位置,比如 var1
和var2
,都有可能被波及:var1?=?1;?var2?=?2Where?do?you?live?
Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
var1?=?2036821625;?var2?=?2003266668
Ok
Segmentation?fault?(core?dumped)
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?#include?
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");
}
運(yùn)行此程序會導(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
文件里,程序運(yùn)行時,就可以調(diào)用這個文件,讀取配置信息。因此,從文件中讀取數(shù)據(jù)的能力對所有程序員都很重要。但是,如果你要讀取的文件不存在怎么辦?在 C 語言中,要讀取文件一般是先使用 fopen
函數(shù)打開文件,然后該函數(shù)返回指向文件的流指針。如果您要讀取的文件不存在或您的程序無法讀取,則 fopen
函數(shù)將返回 NULL
。在這種情況下,我們?nèi)匀粚ζ溥M(jìn)行操作,會發(fā)生什么情況?我們一起來看下:#include?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)你運(yùn)行這個程序時,如果 FILE.TXT 這個文件不存在,那么 pfile 將返回 NULL。在這種情況下我們還對 pfile 進(jìn)行寫操作的話,會立刻導(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)
以確保指針是可以使用的。