Linux下的Valgrind真是利器啊(不知道Valgrind的請自覺檢視參考文獻(1)(2)),幫我找出了不少C++中的記憶體管理錯誤,前一陣子還在糾結為什麼VS 2013下運作良好的程式到了Linux下用g++編譯運作卻崩潰了,給出一堆彙編代碼也看不懂。久久不得解過後,想想肯定是記憶體方面的錯誤,VS在這方面一般都不檢查的,就算你的程式千瘡百孔,各種記憶體洩露、記憶體管理錯誤,隻要不影響運作,沒有讀到不該讀的東西VS就不會告訴你(應該是VS内部沒實作這個記憶體檢測功能),是以用VS寫出的程式可能不是完美或健壯的。
------------------------------------------------------------------------------------------------------------------------------
下面是我通過 Valgrind第一次檢測得到的結果和一點點修改後得到的結果(還沒改完,是以還有不少記憶體洩露問題……):
第一次檢測結果:慘不忍睹,因為程式規模有些大。

根據提示一點點修改過後,雖然還有個别錯誤和記憶體洩露問題,但還在修改中,至少已經能成功運作了……
真感謝Valgrind幫我成功找出了一堆記憶體問題,查找過程中也為自己犯的低級錯誤而感到羞愧,是以記錄下來以便謹記。
這樣的錯誤主要源于我對C++的new/new[]、delete/delete[]機制不熟悉,凡是new/new[]配置設定記憶體的類型變量我一概用delete進行釋放,或者有的變量用malloc進行配置設定,結果釋放的時候卻用delete,導緻申請、釋放很多地方不比對,很多記憶體空間沒能釋放掉。為了維護友善,我後來一律使用new/new[]和delete/delete[],抛棄C中的malloc和free。
如果将使用者new的類型分為基本資料類型和自定義資料類型兩種,那麼對于下面的操作相信大家都很熟悉,也沒有任何問題。
(1)基本資料類型
一維指針:
二維指針:
(2)自定義資料類型
比如下面這樣一個類型:
二維指針:
這沒有任何問題,因為我們都是配套使用new/delete和new[]/delete[]的。這在Valgrind下檢測也是完美通過的,但為什麼要這配套使用呢?原理是什麼?
雖然深究這些東西好像沒什麼實際意義,但對于想深入了解C++内部機制或像我一樣老是釋放出錯導緻大量記憶體洩露的小白程式員還是值得研究的,至少知道了為什麼,以後就不會犯現在的低級錯誤。
參考文獻(3)是這樣描述的:
通常狀況下,編譯器在new的時候會傳回使用者申請的記憶體空間大小,但是實際上,編譯器會配置設定更大的空間,目的就是在delete的時候能夠準确的釋放這段空間。 這段空間在使用者取得的指針之前以及使用者空間末尾之後存放。 實際上:blockSize = sizeof(_CrtMemBlockHeader) + nSize + nNoMansLandSize; 其中,blockSize 是系統所配置設定的實際空間大小,_CrtMemBlockHeader是new的頭部資訊,其中包含使用者申請的空間大小等其他一些資訊。 nNoMansLandSize是尾部的越界校驗大小,一般是4個位元組“FEFEFEFE”,如果使用者越界寫入這段空間,則校驗的時候會assert。nSize才是為我們配置設定的真正可用的記憶體空間。 使用者new的時候分為兩種情況 A. new的是基礎資料類型或者是沒有自定義析構函數的結構 B. new的是有自定義析構函數的結構體或類 這兩者的差別是如果有使用者自定義的析構函數,則delete的時候必須要調用析構函數,那麼編譯器delete時如何知道要調用多少個對象的析構函數呢,答案就是new的時候,如果是情況B,則編譯器會在new頭部之後,使用者獲得的指針之前多配置設定4個位元組的空間用來記錄new的時候的數組大小,這樣delete的時候就可以取到個數并正确的調用。
這段描述可能有些晦澀難懂,參考文獻(4)給了更加詳細的解釋,一點即通。這樣的解釋其實也隐含着一個推論:如果new的是基本資料類型或者是沒有自定義析構函數的結構,那麼這種情況下編譯器不會在使用者獲得的指針之前多配置設定4個位元組,因為這時候delete時不用調用析構函數,也就是不用知道數組個數的大小(因為隻有調用析構函數時才需要知道要調用多少個析構函數,也就是數組的大小),而是直接傳入數組的起始位址進而釋放掉這塊記憶體空間,此時delete與delete[]是等價的。
是以下面的釋放操作也是正确的:
将其放在Valgrind下進行檢測,結果如下:
首先從“All heap blocks were freed -- no leaks are possible”可以看出上面的釋放操作的确是正确的,而不是有些人認為的delete d;隻會釋放d[]的第一個元素的空間,後面的都不會得到釋放。但是從“Mismatched free() / delete / delete []”知道Valgrind實際上是不允許這樣操作的,雖然沒有記憶體洩露問題,但是new[]與delete不比對,這樣的程式設計風格不經意間就容易犯低級錯誤,是以Valgrind報錯了,但是我想Valgrind内部實作應該不會考慮的這麼複雜,它就檢查new是否與delete配對,new[]是否與delete[]配對,而不管有時候new[]與delete配對也不會出現問題的。
綜上所述,給我的經驗就是:在某些情況下,new[]配置設定的記憶體用delete不會出錯,但是大多情況下會産生嚴重的記憶體問題,是以一定要養成将new和delete,new[]和delete[]配套使用的良好程式設計習慣。
比如下面這樣一個程式:
首先對該程式做個扼要的說明:
這裡結構體裡定義零長數組的原因在于我的需求:我在其它地方要用到很大的accept_pair數組,其中隻有個别accept_pair元素中的app_name是有效的(取決于某些值的判斷,如果為true才給app_name指派,如果為false則app_name無意義,為空),是以若是char app_name[20],那麼大部分accept_pair元素都浪費了這20個位元組的空間,是以我在這裡先一個位元組都不配置設定,到時誰需要就給誰配置設定,遵循“按需配置設定”的古老思想。可能有人會想,用char *app_name也可以啊,同樣能實作按需配置設定,是的,隻是多4個位元組而已,屬于替補方法。
在g++下經過測試,沒有什麼問題,能夠正确運作,但用Valgrind檢測時卻報出了一些錯誤,不是記憶體洩露問題,而是記憶體讀寫錯誤:
從檢測報告可以看出:
strcpy(ap->app_name, s);這句是記憶體寫錯誤,printf("app name: %s\n", ap->app_name);這句是記憶體讀錯誤,兩者都說明Valgrind認為ap->app_name所處記憶體空間是不合法的,可是我明明已經為其配置設定了記憶體空間,隻是沒有注明這段空間就是給它用的,難道結構體中零長數組char app_name[0]是不能寫入值的嗎?還是我對零長數組的使用有誤?至今仍不得解,求大神解答……
請看下面這樣一個程式:
雖然有點長,但邏輯很簡單,其中add_size()首先配置設定一個更大的accept_pair數組,将已有的資料全部拷貝進去,然後釋放掉原來的accept_pair數組所占空間,最後将舊的數組指針指向新配置設定的記憶體空間。這是個demo程式,在我看來這段程式是沒有任何記憶體洩露問題的,因為申請的所有記憶體空間最後都會在DFA析構函數中得到釋放。但是Valgrind的檢測報告卻報出了1個記憶體洩露問題(紅色的是程式輸出):
說明add_size()這個函數裡存在用new申請的記憶體空間沒有得到釋放,這一點感覺很費解,開始以為tmp_states指針所指向的資料賦給accept_states後沒有及時釋放導緻的,于是我最後加了句delete tmp_states;結果招緻更多的錯誤。相信不是Valgrind誤報,說明我對C++的new和delete機制還是不明不白,一些于我而言不明是以的記憶體洩露問題真心不得解,希望有人能夠告訴我是哪裡的問題?
第3個問題,是有兩個洩露 DFA::add_state裡面直接 accept_states[index] = new accept_pair(true, true); 如果原來的accept_states[index]不為NULL就洩露了 而在DFA::add_size裡面, for (int s = 0; s < size + _size; s++) tmp_states[s] = new accept_pair(false, false); 對新配置設定的tmp_states的每一個元素都new了一個新的accept_pair 是以在main函數裡面dfa->add_size(2);以後,總共有5個成員,而且5個都不為NULL 之後 dfa->add_state(3, s); dfa->add_state(4, s); 結果就導緻了index為3和4的原先的對象洩露了 你的系統是32位的,是以一個accept_pair大小是8byte,兩個對象就是16byte
解決方案也很簡單,修改add_size函數,重新申請空間時僅為已有的accept_pair資料申請空間,其它的初始化為NULL,這樣在需要時才在add_state裡面申請空間,也就是修改add_size函數如下: