程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> c語言常見內存錯誤大全

c語言常見內存錯誤大全

編輯:關於C語言
 

隨著諸如代碼重構和單元測試等方法引入實踐,調試技能漸漸弱化了,甚至有人主張廢除調試器。這是有道理的,原因在於調試的代價往往太大了,特別是調試系統集成之後的BUG,一個BUG花了幾天甚至數周時間並非罕見。

 

而這些難以定位的BUG基本上可以歸為兩類:內存錯誤和並發問題。而又以內存錯誤最為普遍,即使是久經沙場的老手,也有時也難免落入陷阱。前事不忘,後世之師,了解這些常見的錯誤,在編程時就加以注意,把出錯的概率降到最低,可以節省不少時間。

 

這些列舉一些常見的內存錯誤,供新手參考。

 

1. 內存洩露。

大家都知道,在堆上分配的內存,如果不再使用了,應該把它釋放掉,以便後面其它地方可以重用。在C/C++中,內存管理器不會幫你自動回收不再使用的內存。如果你忘了釋放不再使用的內存,這些內存就不能被重用,就造成了所謂的內存洩露。

 

把內存洩露列為首位,倒並不是因為它有多麼嚴重的後果,而因為它是最為常見的一類錯誤。一兩處內存洩露通常不至於讓程序崩潰,也不會出現邏輯上的錯誤,加上進程退出時,系統會自動釋放該進程所有相關的內存,所以內存洩露的後果相對來說還是比較溫和的。當然了,量變會產生質變,一旦內存洩露過多以致於耗盡內存,後續內存分配將會失敗,程序可能因此而崩潰。

 

現在的PC機內存夠大了,加上進程有獨立的內存空間,對於一些小程序來說,內存洩露已經不是太大的威脅。但對於大型軟件,特別是長時間運行的軟件,或者嵌入式系統來說,內存洩露仍然是致命的因素之一。

 

不管在什麼情況下,采取比較謹慎的態度,杜絕內存洩露的出現,都是可取的。相反,認為內存有的是,對內存洩露放任自流都不是負責的。盡管一些工具可以幫助我們檢查內存洩露問題,我認為還是應該在編程時就仔細一點,及早排除這類錯誤,工具只是用作驗證的手段。

 

2. 內存越界訪問。

內存越界訪問有兩種:一種是讀越界,即讀了不屬於自己的數據,如果所讀的內存地址是無效的,程度立刻就崩潰了。如果所讀內存地址是有效的,在讀的時候不會出問題,但由於讀到的數據是隨機的,它會產生不可預料的後果。另外一種是寫越界,又叫緩沖區溢出。所寫入的數據對別人來說是隨機的,它也會產生不可預料的後果。

 

內存越界訪問造成的後果非常嚴重,是程序穩定性的致命威脅之一。更麻煩的是,它造成的後果是隨機的,表現出來的症狀和時機也是隨機的,讓BUG的現象和本質看似沒有什麼聯系,這給BUG的定位帶來極大的困難。

 

一些工具可以夠幫助檢查內存越界訪問的問題,但也不能太依賴於工具。內存越界訪問通常是動態出現的,即依賴於測試數據,在極端的情況下才會出現,除非精心設計測試數據,工具也無能為力。工具本身也有一些限制,甚至在一些大型項目中,工具變得完全不可用。比較保險的方法還是在編程是就小心,特別是對於外部傳入的參數要仔細檢查。

 

3. 野指針。

野指針是指那些你已經釋放掉的內存指針。當你調用free(p)時,你真正清楚這個動作背後的內容嗎?你會說p指向的內存被釋放了。沒錯,p本身有變化嗎?答案是p本身沒有變化。它指向的內存仍然是有效的,你繼續讀寫p指向的內存,沒有人能攔得住你。

 

釋放掉的內存會被內存管理器重新分配,此時,野指針指向的內存已經被賦予新的意義。對野指針指向內存的訪問,無論是有意還是無意的,都為此會付出巨大代價,因為它造成的後果,如同越界訪問一樣是不可預料的。

 

釋放內存後立即把對應指針置為空值,這是避免野指針常用的方法。這個方法簡單有效,只是要注意,當然指針是從函數外層傳入的時,在函數內把指針置為空值,對外層的指針沒有影響。比如,你在析構函數裡把this指針置為空值,沒有任何效果,這時應該在函數外層把指針置為空值。

 

4. 訪問空指針。

空指針在C/C++中占有特殊的地址,通常用來判斷一個指針的有效性。空指針一般定義為0。現代操作系統都會保留從0開始的一塊內存,至於這塊內存有多大,視不同的操作系統而定。一旦程序試圖訪問這塊內存,系統就會觸發一個異常。

 

操作系統為什麼要保留一塊內存,而不是僅僅保留一個字節的內存呢?原因是:一般內存管理都是按頁進行管理的,無法單純保留一個字節,至少要保留一個頁面。保留一塊內存也有額外的好處,可以檢查諸如p=NULL; p[1]之類的內存錯誤。

 

在一些嵌入式系統(如arm7)中,從0開始的一塊內存是用來安裝中斷向量的,沒有MMU的保護,直接訪問這塊內存好像不會引發異常。不過這塊內存是代碼段的,不是程序中有效的變量地址,所以用空指針來判斷指針的有效性仍然可行。

 

在訪問指針指向的內存時,在確保指針不是空指針。訪問空指針指向的內存,通常會導致程度崩潰,或者不可預料的錯誤。

 

5. 引用未初始化的變量。

未初始化變量的內容是隨機的(像VC一類的編譯器會把它們初始化為固定值,如0xcc),使用這些數據會造成不可預料的後果,調試這樣的BUG也是非常困難的。

 

對於態度嚴謹的程度員來說,防止這類BUG非常容易。在聲明變量時就對它進行初始化,是一個編程的好習慣。另外也要重視編譯器的警告信息,發現有引用未初始化的變量,立即修改過來。

 

6. 不清楚指針運算。

對於一些新手來說,指針常常讓他們犯糊塗。

 

比如int *p = …; p+1等於(size_t)p + 1嗎

老手自然清楚,新手可能就搞不清了。事實上, p+n 等於 (size_t)p + n * sizeof(*p)

 

指針是C/C++中最有力的武器,功能非常強大,無論是變量指針還是函數指針,都應該掌握都非常熟練。只要有不確定的地方,馬上寫個小程序驗證一下。對每一個細節都了然於胸,在編程時會省下不少時間。

 

7. 結構的成員順序變化引發的錯誤。

在初始化一個結構時,老手可能很少像新手那樣老老實實的,一個成員一個成員的為結構初始化,而是采用快捷方式,如:

Struct s

{

int l;

char* p;

};

 

int main(int argc, char* argv[])

{

struct s s1 = {4, "abcd"};

return 0;

}

 

以上這種方式是非常危險的,原因在於你對結構的內存布局作了假設。如果這個結構是第三方提供的,他很可能調整結構中成員的相對位置。而這樣的調整往往不會在文檔中說明,你自然很少去關注。如果調整的兩個成員具有相同數據類型,編譯時不會有任何警告,而程序的邏輯上可能相距十萬八千裡了。

 

正確的初始化方法應該是(當然,一個成員一個成員的初始化也行):

struct s

{

int l;

char* p;

};

 

int main(int argc, char* argv[])

{

struct s s1 = {.l=4, .p = "abcd"};

struct s s2 = {l:4, p:"abcd"};

 

return 0;

}

 

8. 結構的大小變化引發的錯誤。

我們看看下面這個例子:

struct base

{

int n;

};

 

struct s

{

struct base b;

int m;

};

 

在OOP中,我們可以認為第二個結構繼承了第一結構,這有什麼問題嗎?當然沒有,這是C語言中實現繼承的基本手法。

 

現在假設第一個結構是第三方提供的,第二個結構是你自己的。第三方提供的庫是以DLL方式分發的,DLL最大好處在於可以獨立替換。但隨著軟件的進化,問題可能就來了。

 

當第三方在第一個結構中增加了一個新的成員int k;,編譯好後把DLL給你,你直接給了客戶了。程序加載時不會有任何問題,在運行邏輯可能完全改變!原因是兩個結構的內存布局重疊了。解決這類錯誤的唯一辦法就是全部重新相關的代碼。

 

解決這類錯誤的唯一辦法就是重新編譯全部代碼。由此看來,DLL並不見得可以動態替換,如果你想了解更多相關內容,建議閱讀《COM本質論》。

 

9. 分配/釋放不配對。

大家都知道malloc要和free配對使用,new要和delete/delete[]配對使用,重載了類new操作,應該同時重載類的delete/delete[]操作。這些都是書上反復強調過的,除非當時暈了頭,一般不會犯這樣的低級錯誤。

 

而有時候我們卻被蒙在鼓裡,兩個代碼看起來都是調用的free函數,實際上卻調用了不同的實現。比如在Win32下,調試版與發布版,單線程與多線程是不同的運行時庫,不同的運行時庫使用的是不同的內存管理器。一不小心鏈接錯了庫,那你就麻煩了。程序可能動則崩潰,原因在於在一個內存管理器中分配的內存,在另外一個內存管理器中釋放時出現了問題。

 

10. 返回指向臨時變量的指針

大家都知道,棧裡面的變量都是臨時的。當前函數執行完成時,相關的臨時變量和參數都被清除了。不能把指向這些臨時變量的指針返回給調用者,這樣的指針指向的數據是隨機的,會給程序造成不可預料的後果。

 

下面是個錯誤的例子:

char* get_str(void)

{

char str[] = {"abcd"};

 

return str;

}

 

int main(int argc, char* argv[])

{

char* p = get_str();

 

printf("%s\n", p);

 

return 0;

}

 

 

下面這個例子沒有問題,大家知道為什麼嗎?

char* get_str(void)

{

char* str = {"abcd"};

 

return str;

}

 

int main(int argc, char* argv[])

{

char* p = get_str();

 

printf("%s\n", p);

 

return 0;

}

 
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved