您好,登錄后才能下訂單哦!
C語言編程中常見的五種錯誤及對應解決方法,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
前言:
C 語言有時名聲不太好,因為它不像近期的編程語言(比如 Rust)那樣具有內(nèi)存安全性。但是通過額外的代碼,一些最常見和嚴重的 C 語言錯誤是可以避免的。
即使是最好的程序員也無法完全避免錯誤。這些錯誤可能會引入安全漏洞、導致程序崩潰或產(chǎn)生意外操作,具體影響要取決于程序的運行邏輯。
下文講解了可能影響應用程序的五個錯誤以及避免它們的方法:
程序啟動時,系統(tǒng)會為其分配一塊內(nèi)存以供存儲數(shù)據(jù)。這意味著程序啟動時,變量將獲得內(nèi)存中的一個隨機值。
有些編程環(huán)境會在程序啟動時特意將內(nèi)存“清零”,因此每個變量都得以有初始的零值。程序中的變量都以零值作為初始值,聽上去是很不錯的。但是在 C 編程規(guī)范中,系統(tǒng)并不會初始化變量。
看一下這個使用了若干變量和兩個數(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; }
這個程序不會初始化變量,所以變量以系統(tǒng)內(nèi)存中的隨機值作為初始值。在我的 Linux
系統(tǒng)上編譯和運行這個程序,會看到一些變量恰巧有“零”值,但其他變量并沒有:
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
數(shù)組中,大多數(shù)元素也恰好從零值開始,只有第三個元素的初始值為 4199024
。
在不同的系統(tǒng)上編譯相同的程序,可以進一步顯示未初始化變量的危險性。不要誤以為“全世界都在運行 Linux”,你的程序很可能某天在其他平臺上運行。例如,下面是在 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
永遠都要記得初始化程序的變量。如果你想讓變量將以零值作為初始值,請額外添加代碼將零分配給該變量。預先編好這些額外的代碼,這會有助于減少日后讓人頭疼的調(diào)試過程。
C 語言中,數(shù)組索引從零開始。這意味著對于長度為 10
的數(shù)組,索引是從 0 到 9;長度為 1000 的數(shù)組,索引則是從 0 到 999。
程序員有時會忘記這一點,他們從索引 1 開始引用數(shù)組,產(chǎn)生了“大小差一”off by one
錯誤。在長度為 5 的數(shù)組中,程序員在索引“5”處使用的值,實際上并不是數(shù)組的第 5 個元素。相反,它是內(nèi)存中的一些其他值,根本與此數(shù)組無關(guān)。
這是一個數(shù)組越界的示例程序。該程序使用了一個只含有 5 個元素的數(shù)組,但卻引用了該范圍之外的數(shù)組元素:
#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ù)組的所有值(從索引 0 到 4),然后從索引 0 開始讀取,結(jié)尾是索引 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
引用數(shù)組時,始終要記得追蹤數(shù)組大小。將數(shù)組大小存儲在變量中;不要對數(shù)組大小進行硬編碼hard-code
。否則,如果后期該標識符指向另一個不同大小的數(shù)組,卻忘記更改硬編碼的數(shù)組長度時,程序就可能會發(fā)生數(shù)組越界。
字符串只是特定類型的數(shù)組。在 C
語言中,字符串是一個由 char
類型值組成的數(shù)組,其中用一個零字符表示字符串的結(jié)尾。
因此,與數(shù)組一樣,要注意避免超出字符串的范圍。有時也稱之為 字符串溢出。
使用 gets
函數(shù)讀取數(shù)據(jù)是一種很容易發(fā)生字符串溢出的行為方式。gets
函數(shù)非常危險,因為它不知道在一個字符串中可以存儲多少數(shù)據(jù),只會機械地從用戶那里讀取數(shù)據(jù)。如果用戶輸入像 foo
這樣的短字符串,不會發(fā)生意外;但是當用戶輸入的值超過字符串長度時,后果可能是災難性的。
下面是一個使用 gets
函數(shù)讀取城市名稱的示例程序。在這個程序中,我還添加了一些未使用的變量,來展示字符串溢出對其他數(shù)據(jù)的影響:
#include <stdio.h> #include <string.h> int 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("<%s> 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? Raleigh <Raleigh> is length 7 var1 = 1; var2 = 2 Ok
威爾士的小鎮(zhèn) Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
有著世界上最長的名字之一。這個字符串有 58 個字符,遠遠超出了 name
變量中保留的 10 個字符。結(jié)果,程序?qū)⒅荡鎯υ趦?nèi)存的其他區(qū)域,覆蓋了 var1
和 var2
的值:
var1 = 1; var2 = 2 Where do you live? Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch <Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch> is length 58 var1 = 2036821625; var2 = 2003266668 Ok Segmentation fault (core dumped)
在運行結(jié)束之前,程序會用長字符串覆蓋內(nèi)存的其他部分區(qū)域。注意,var1
和 var2
的值不再是起始的 1 和 2。
避免使用 gets
函數(shù),改用更安全的方法來讀取用戶數(shù)據(jù)。例如,getline
函數(shù)會分配足夠的內(nèi)存來存儲用戶輸入,因此不會因輸入長值而發(fā)生意外的字符串溢出。
“分配的內(nèi)存要手動釋放”是良好的 C 語言編程原則之一。程序可以使用 malloc
函數(shù)為數(shù)組和字符串分配內(nèi)存,該函數(shù)會開辟一塊內(nèi)存,并返回一個指向內(nèi)存中起始地址的指針。之后,程序可以使用 free
函數(shù)釋放內(nèi)存,該函數(shù)會使用指針將內(nèi)存標記為未使用。
但是,你應該只使用一次 free
函數(shù)。第二次調(diào)用 free
會導致意外的后果,可能會毀掉你的程序。下面是一個針對此點的簡短示例程序。程序分配了內(nèi)存,然后立即釋放了它。但為了模仿一個健忘但有條理的程序員,我在程序結(jié)束時又一次釋放了內(nèi)存,導致兩次釋放了相同的內(nèi)存:
#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"); }
運行這個程序會導致第二次使用 free 函數(shù)時出現(xiàn)戲劇性的失敗:
malloc an array ... malloc succeeded Free the array... Free the array... free(): double free detected in tcache 2 Aborted (core dumped)
要記得避免在數(shù)組或字符串上多次調(diào)用 free
。將 malloc
和 free
函數(shù)定位在同一個函數(shù)中,這是避免重復釋放內(nèi)存的一種方法。
例如,一個紙牌游戲程序可能會在主函數(shù)中為一副牌分配內(nèi)存,然后在其他函數(shù)中使用這副牌來玩游戲。記得在主函數(shù),而不是其他函數(shù)中釋放內(nèi)存。將 malloc
和 free
語句放在一起有助于避免多次釋放內(nèi)存。
文件是一種便捷的數(shù)據(jù)存儲方式。例如,你可以將程序的配置數(shù)據(jù)存儲在 config.dat
文件中。Bash shell
會從用戶家目錄中的 .bash_profile
讀取初始化腳本。GNU Emacs
編輯器會尋找文件 .emacs
以從中確定起始值。而 Zoom 會議客戶端使用 zoomus.conf
文件讀取其程序配置。
所以,從文件中讀取數(shù)據(jù)的能力幾乎對所有程序都很重要。但是假如要讀取的文件不存在,會發(fā)生什么呢?
在 C 語言中讀取文件,首先要用 fopen
函數(shù)打開文件,該函數(shù)會返回指向文件的流指針。你可以結(jié)合其他函數(shù),使用這個指針來讀取數(shù)據(jù),例如 fgetc 會逐個字符地讀取文件。
如果要讀取的文件不存在或程序沒有讀取權(quán)限,fopen
函數(shù)會返回 NULL
作為文件指針,這表示文件指針無效。但是這里有一個示例程序,它機械地直接去讀取文件,不檢查 fopen
是否返回了 NULL:
#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; }
當你運行這個程序時,第一次調(diào)用 fgetc 會失敗,程序會立即中止:
Open the FILE.TXT file ... Now display the contents of FILE.TXT ... Segmentation fault (core dumped)
始終檢查文件指針以確保其有效。例如,在調(diào)用 fopen
打開一個文件后,用類似 if (pfile != NULL)
的語句檢查指針,以確保指針是可以使用的。
人都會犯錯,最優(yōu)秀的程序員也會產(chǎn)生編程錯誤。但是,遵循上面這些準則,添加一些額外的代碼來檢查這五種類型的錯誤,就可以避免最嚴重的 C 語言編程錯誤。提前編寫幾行代碼來捕獲這些錯誤,可能會幫你節(jié)省數(shù)小時的調(diào)試時間。
看完上述內(nèi)容是否對您有幫助呢?如果還想對相關(guān)知識有進一步的了解或閱讀更多相關(guān)文章,請關(guān)注億速云行業(yè)資訊頻道,感謝您對億速云的支持。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。