對(duì)C語(yǔ)言入門(mén)程序員來(lái)說(shuō),管理和使用虛擬存儲(chǔ)器可能是個(gè)困難的,容易出錯(cuò)的任務(wù)。與存儲(chǔ)器有關(guān)的錯(cuò)誤屬于那些最令人驚恐的錯(cuò)誤,因?yàn)樗鼈兘?jīng)常在時(shí)間和空間上,都在距錯(cuò)誤源一段距離之后,才表現(xiàn)出來(lái)。
將錯(cuò)誤的數(shù)據(jù)編寫(xiě)到錯(cuò)誤的位置,你的程序可能在最終失敗之前運(yùn)行了好幾個(gè)小時(shí),且使程序中止的位置距離錯(cuò)誤的位置已經(jīng)很遠(yuǎn)了。
1、間接引用壞指針
在進(jìn)程的虛擬地址空間中有較大的漏洞,沒(méi)有映射到任何有意義的數(shù)據(jù)。如果我們?cè)噲D間接引用一個(gè)指向這些洞的指針,那么操作系統(tǒng)就會(huì)以段異常終止我們的程序。
而且,虛擬存儲(chǔ)器的某些區(qū)域是只讀的。試圖寫(xiě)這些區(qū)域?qū)⒃斐梢员Wo(hù)異常終止這個(gè)程序。
間接引用壞指針的一個(gè)常見(jiàn)示例是經(jīng)典的scanf錯(cuò)誤。假設(shè)我們想要使用scanf從stdin讀一個(gè)整數(shù)到變量。做這件事情正確的方法是傳遞給scanf一個(gè)格式串和變量的地址:
????
然而,對(duì)于c語(yǔ)言程序員初學(xué)者而言,很容易傳遞val的內(nèi)容,而不是它的地址:
????
在這種情況下,scanf將把val的內(nèi)容解釋為一個(gè)地址,并試圖將一個(gè)字寫(xiě)到這個(gè)位置。在最好的情況下,程序立即以異常終止。
在最糟糕的情況下,val的內(nèi)容對(duì)應(yīng)于虛擬存儲(chǔ)器的某個(gè)合法的讀/寫(xiě)區(qū)域,于是我們就覆蓋了存儲(chǔ)器,這通常會(huì)在相當(dāng)以后造成災(zāi)難性的、令人困惑的后果。
2、讀未初始化的存儲(chǔ)器
雖然.bss存儲(chǔ)器位置(諸如未初始化的全局C變量)總是被加載器初始化為零,但是對(duì)于堆存儲(chǔ)器卻并不是這樣的。一個(gè)常見(jiàn)的錯(cuò)誤就是假設(shè)堆存儲(chǔ)器被初始化為零:
????
在這個(gè)示例中,程序員不正確地假設(shè)向量y被初始化為零。正確的實(shí)現(xiàn)方式是在for循環(huán)時(shí)將y[i]設(shè)置為零,或使用calloc。
3、允許棧緩沖區(qū)溢出
如果一個(gè)程序不檢查輸入串的大小就寫(xiě)入棧中的目標(biāo)換成區(qū),那么這個(gè)程序就會(huì)有緩沖區(qū)溢出錯(cuò)誤。例如,下面的函數(shù)就有緩沖區(qū)錯(cuò)誤,因?yàn)間ets函數(shù)拷貝一個(gè)任意長(zhǎng)度的串到緩沖區(qū)。為了糾正這個(gè)錯(cuò)誤,我們必須使用fgets函數(shù),這個(gè)函數(shù)限制了輸入串的大小:
4、假設(shè)指針和它們指向的對(duì)象是相同大小的
一種常見(jiàn)的錯(cuò)誤是假設(shè)指向?qū)ο蟮闹羔樅退鼈兯赶虻膶?duì)象是相同大小的:
????
這里的目的是創(chuàng)建一個(gè)由n個(gè)指針組成的數(shù)組,每個(gè)指針都指向一個(gè)包含m個(gè)int的數(shù)組。然而,因?yàn)槌绦騿T將int **A = (int **)malloc(n * sizeof(int));中將sizeof(int)寫(xiě)成了sizeof(int),代碼實(shí)際創(chuàng)建的是一個(gè)int的數(shù)組。這段代碼只有在int和指向int的指針大小相同的機(jī)器上運(yùn)行良好。
但是,如果我們?cè)谙馎lpha這樣的機(jī)器上運(yùn)行這段代碼,其中指針大于int,那么在for(i = 0; i < n; i++)? A[i] = (int *)malloc(m * sizeof(int));將寫(xiě)到超過(guò)A數(shù)組末端的地方。因?yàn)檫@些字中的一個(gè)很可能是分配塊的邊界標(biāo)記腳部,所以我們可能不會(huì)發(fā)現(xiàn)這個(gè)錯(cuò)誤,而沒(méi)有任何明顯的原因。
5、造成錯(cuò)位錯(cuò)位
錯(cuò)位錯(cuò)誤是另一種很常見(jiàn)的覆蓋錯(cuò)誤發(fā)生的原因:
????
這是前面程序的另一個(gè)版本。這里我們創(chuàng)建了一個(gè)n個(gè)元素的指針數(shù)組,但是隨后試圖初始化這個(gè)數(shù)組的n+1個(gè)元素,在這個(gè)過(guò)程中覆蓋了A數(shù)組后面的某個(gè)存儲(chǔ)器。
6、引用指針,而不是它所指向的對(duì)象
如果我們不太注意C操作符的優(yōu)先級(jí)和結(jié)合性,我們就會(huì)錯(cuò)誤地操作指針,而不是期望操作指針?biāo)赶虻膶?duì)象。比如,考慮下面的函數(shù),其目的是刪除一個(gè)有*size項(xiàng)的二叉堆里的第一項(xiàng),然后對(duì)剩下的*size-1項(xiàng)重新建堆。
???
*size—目的是減少size指針指向的整數(shù)的值。然而,因?yàn)橐辉?運(yùn)算符優(yōu)先級(jí)相同,從右向左結(jié)合,所以代碼實(shí)際減少的是指針自己的值,而不是它所指向的整數(shù)的值。
如果幸運(yùn)的話,程序會(huì)立即失敗,但是更有可能發(fā)生的是,當(dāng)程序在它執(zhí)行過(guò)程的很后面產(chǎn)生一個(gè)不正確的結(jié)果時(shí),我們只能在那里抓腦袋了。這里的原則是如果你對(duì)優(yōu)先級(jí)和結(jié)合性有疑問(wèn),就使用括號(hào)。使用表達(dá)式(*size)--。
7、誤解指針運(yùn)算
另一種常見(jiàn)的錯(cuò)誤是忘記了指針的算術(shù)操作是以它們指向的對(duì)象的大小為單位來(lái)進(jìn)行的,而這種大小單位并不一定是字節(jié)。例如,下面函數(shù)的目的是掃描一個(gè)int的數(shù)組,并返回一個(gè)指針,指向val的首次出現(xiàn):
8、引用不存在的變量
沒(méi)有太多經(jīng)驗(yàn)的C程序員不理解棧的規(guī)則,有時(shí)會(huì)引用不再合法的本地變量,如下列所示:
????
這個(gè)函數(shù)返回一個(gè)指針,指向棧里的一個(gè)局部變量,然后彈出它的棧幀。盡管p仍然指向一個(gè)合法的存儲(chǔ)器地址,但是它已經(jīng)不再指向一個(gè)合法的變量了。
當(dāng)以后在程序中調(diào)用其他函數(shù)時(shí),存儲(chǔ)器將重用它們的幀棧。后來(lái),如果程序分配某個(gè)值給*p,那么它可能實(shí)際正在修改另一個(gè)函數(shù)的幀棧中的一個(gè)條目,從而帶來(lái)潛在地災(zāi)難性的、令人困惑的后果。
9、引用空閑堆塊中的數(shù)據(jù)
一個(gè)相似的錯(cuò)誤是引用已被釋放了的堆塊中的數(shù)據(jù)。如下面的示例,示例中分配了一個(gè)整數(shù)數(shù)組x,之后釋放了塊x,最后又引用了它。
10、引起存儲(chǔ)器泄漏
存儲(chǔ)器泄漏是緩慢、隱形的殺手,當(dāng)程序員不小心忘記釋放已分配塊,而在堆里創(chuàng)建了垃圾時(shí),會(huì)發(fā)生這種問(wèn)題。例如,下面的函數(shù)分配了一個(gè)堆塊x,然后不釋放它就返回。
????
如果leak經(jīng)常被調(diào)用,堆里就會(huì)充滿(mǎn)了垃圾,最糟糕的情況下,會(huì)占有整個(gè)虛擬地址空間。對(duì)于像守護(hù)進(jìn)程和服務(wù)器這樣的程序來(lái)說(shuō),存儲(chǔ)器泄漏是特別嚴(yán)重的,根據(jù)定義這些程序是不會(huì)終止的。
審核編輯:劉清
-
存儲(chǔ)器
+關(guān)注
關(guān)注
38文章
7496瀏覽量
163927 -
C語(yǔ)言
+關(guān)注
關(guān)注
180文章
7605瀏覽量
137003 -
虛擬機(jī)
+關(guān)注
關(guān)注
1文章
918瀏覽量
28232
原文標(biāo)題:C程序中常見(jiàn)的與內(nèi)存相關(guān)的錯(cuò)誤
文章出處:【微信號(hào):c-stm32,微信公眾號(hào):STM32嵌入式開(kāi)發(fā)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論