關於C/C++的記憶體分配
在C和C++語言的學習和使用中,尤其是科學計算程式設計中,記憶體的合理分配常常是一個很頭疼的問題。你無法在你的程式中使用,例如double a[2000][3000]這樣的超大靜態陣列變數,因為這已經超出普通編譯器預設的棧大小更或者會受到系統等因素的影響而無法使用,必須使用動態分配記憶體的技術,這樣就可以不受限制了。(前提是系統記憶體足夠)
一、關於記憶體 1、記憶體分配方式 記憶體有三種: (1)從靜態儲存區域分配。記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式的整個執行期間都存在 。例如全域性變數,static變數。 (2)在棧上建立。在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些存 儲單元自動被釋放。棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限。 (3) 從堆上分配,亦稱動態記憶體分配。程式在執行的時候用malloc或new申請任意多少的記憶體,程式設計師自 己負責在何時用free或delete釋放記憶體。動態記憶體的生存期由我們決定,使用非常靈活,但問題也最多。 2.記憶體使用錯誤 發生記憶體錯誤是件非常麻煩的事情。編譯器不能自動發現這些錯誤,通常是在程式執行時才能捕捉到。 而這些錯誤大多沒有明顯的症狀,時隱時現,增加了改錯的難度。有時使用者怒氣衝衝地把你找來,程式卻沒有 發生任何問題,你一走,錯誤又發作了。 常見的記憶體錯誤及其對策如下: * 記憶體分配未成功,卻使用了它。 程式設計新手常犯這種錯誤,因為他們沒有意識到記憶體分配會不成功。常用解決辦法是,在使用記憶體之前檢查 指標是否為NULL。如果是用malloc或new來申請記憶體,應該用if(p==NULL) 或if(p!=NULL)進行防錯處理。 * 記憶體分配雖然成功,但是尚未初始化就引用它。 犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以為記憶體的預設初值全為零,導致引用初值 錯誤(例如陣列)。 記憶體的預設初值究竟是什麼並沒有統一的標準,儘管有些時候為零值,我們寧可信其無不 可信其有。所以無論用何種方式建立陣列,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。 * 記憶體分配成功並且已經初始化,但操作越過了記憶體的邊界。 例如在使用陣列時經常發生下標“多1”或者“少1”的操作。特別是在for迴圈語句中,迴圈次數很容易搞 錯,導致陣列操作越界。 * 忘記了釋放記憶體,造成記憶體洩露。 含有這種錯誤的函式每被呼叫一次就丟失一塊記憶體。剛開始時系統的記憶體充足,你看不到錯誤。終有一次 程式突然死掉,系統出現提示:記憶體耗盡。 動態記憶體的申請與釋放必須配對,程式中malloc與free的使用次數一定要相同,否則肯定有錯誤 (new/delete同理)。 * 釋放了記憶體卻繼續使用它。 有三種情況: (1)程式中的物件呼叫關係過於複雜,實在難以搞清楚某個物件究竟是否已經釋放了記憶體,此時應該重新 設計資料結構,從根本上解決物件管理的混亂局面。 (2)函式的return語句寫錯了,注意不要返回指向“棧記憶體”的“指標”或者“引用”,因為該記憶體在函 數體結束時被自動銷燬。 (3)使用free或delete釋放了記憶體後,沒有將指標設定為NULL。導致產生“野指標”。 【規則1】用malloc或new申請記憶體之後,應該立即檢查指標值是否為NULL。防止使用指標值為NULL的記憶體 【規則2】不要忘記為陣列和動態記憶體賦初值。防止將未被初始化的記憶體作為右值使用。 【規則3】避免陣列或指標的下標越界,特別要當心發生“多1”或者“少1”操作。 【規則4】動態記憶體的申請與釋放必須配對,防止記憶體洩漏。 【規則5】用free或delete釋放了記憶體之後,立即將指標設定為NULL,防止產生“野指標”。
二. 詳解new,malloc,GlobalAlloc 1. new new和delete運算子用於動態分配和撤銷記憶體的運算子 new用法: 1> 開闢單變數地址空間 1)new int; //開闢一個存放陣列的儲存空間,返回一個指向該儲存空間的地址.int *a = new int 即為將一個int型別的地址賦值給整型指標a. 2)int *a = new int(5) 作用同上,但是同時將整數賦值為5 2> 開闢陣列空間 一維: int *a = new int[100];開闢一個大小為100的整型陣列空間 一般用法: new 型別 [初值] delete用法: 1> int *a = new int; delete a; //釋放單個int的空間 2>int *a = new int[5]; delete [] a; //釋放int陣列空間 要訪問new所開闢的結構體空間,無法直接通過變數名進行,只能通過賦值的指標進行訪問. 用new和delete可以動態開闢,撤銷地址空間.在程式設計序時,若用完一個變數(一般是暫時儲存的陣列), 下次需要再用,但卻又想省去重新初始化的功夫,可以在每次開始使用時開闢一個空間,在用完後撤銷它.
2. malloc 原型:extern void *malloc(unsigned int num_bytes); 用法:#i nclude 或#i nclude 功能:分配長度為num_bytes位元組的記憶體塊 說明:如果分配成功則返回指向被分配記憶體的指標,否則返回空指標NULL。 當記憶體不再使用時,應使用free()函式將記憶體塊釋放。 malloc的語法是:指標名=(資料型別*)malloc(長度),(資料型別*)表示指標. 說明:malloc 向系統申請分配指定size個位元組的記憶體空間。返回型別是 void* 型別。void* 表示未確定型別 的指標。C,C++規定,void* 型別可以強制轉換為任何其它型別的指標。
malloc()函式的工作機制 malloc函式的實質體現在,它有一個將可用的記憶體塊連線為一個長長的列表的所謂空閒連結串列。呼叫malloc 函式時,它沿連線表尋找一個大到足以滿足使用者請求所需要的記憶體塊。然後,將該記憶體塊一分為二(一塊的大 小與使用者請求的大小相等,另一塊的大小就是剩下的位元組)。接下來,將分配給使用者的那塊記憶體傳給使用者,並 將剩下的那塊(如果有的話)返回到連線表上。呼叫free函式時,它將使用者釋放的記憶體塊連線到空閒鏈上。到 最後,空閒鏈會被切成很多的小記憶體片段,如果這時使用者申請一個大的記憶體片段,那麼空閒鏈上可能沒有可以 滿足使用者要求的片段了。於是,malloc函式請求延時,並開始在空閒鏈上翻箱倒櫃地檢查各記憶體片段,對它們 進行整理,將相鄰的小空閒塊合併成較大的記憶體塊。
和new的不同
從函式宣告上可以看出。malloc 和 new 至少有兩個不同: new 返回指定型別的指標,並且可以自動計算所需 要大小。比如: int *p; p = new int; //返回型別為int* 型別(整數型指標),分配大小為 sizeof(int); 或: int* parr; parr = new int [100]; //返回型別為 int* 型別(整數型指標),分配大小為 sizeof(int) * 100; 而 malloc 則必須由我們計算要位元組數,並且在返回後強行轉換為實際型別的指標。 int* p; p = (int *) malloc (sizeof(int)); 第一、malloc 函式返回的是 void * 型別,如果你寫成:p = malloc (sizeof(int)); 則程式無法通過編譯,報錯:“不能將 void* 賦值給 int * 型別變數”。所以必須通過 (int *) 來將強制轉換。 第二、函式的實參為 sizeof(int) ,用於指明一個整型資料需要的大小。如果你寫成: int* p = (int *) malloc (1); 程式碼也能通過編譯,但事實上只分配了1個位元組大小的記憶體空間,當你往裡頭存入一個整數,就會有3個位元組無家可歸,而直接“住進鄰居家”!造成的結果是後面的記憶體中原有資料內容全部被清空。
3. GlobalAlloc
VC中關於GlobalAlloc,GlobalLock,GlobalUnLock 呼叫GlobalAlloc函式分配一塊記憶體,該函式會返回分配的記憶體控制代碼。 呼叫GlobalLock函式鎖定記憶體塊,該函式接受一個記憶體控制代碼作為引數,然後返回一個指向被鎖定的記憶體塊的指標。 您可以用該指標來讀寫記憶體。 呼叫GlobalUnlock函式來解鎖先前被鎖定的記憶體,該函式使得指向記憶體塊的指標無效。 呼叫GlobalFree函式來釋放記憶體塊。您必須傳給該函式一個記憶體控制代碼。 GlobalAlloc 說明 分配一個全域性記憶體塊 返回值 Long,返回全域性記憶體控制代碼。零表示失敗。會設定GetLastError
引數表 引數 型別及說明 wFlags Long,對分配的記憶體型別進行定義的常數標誌,如下所示: GMEM_FIXED 分配一個固定記憶體塊 GMEM_MOVEABLE 分配一個可移動記憶體塊 GMEM_DISCARDABLE 分配一個可丟棄記憶體塊 GMEM_NOCOMPACT 堆在這個函式呼叫期間不進行累積 GMEM_NODISCARD 函式呼叫期間不丟棄任何記憶體塊 GMEM_ZEROINIT 新分配的記憶體塊全部初始化成零 dwBytes Long,要分配的字元數
GlobalLock 函式功能描述:鎖定一個全域性的記憶體物件,返回指向該物件的第一個位元組的指標 函式原型: LPVOID GlobalLock( HGLOBAL hMem ) 引數:hMem:全域性記憶體物件的控制代碼。這個控制代碼是通過GlobalAlloc或GlobalReAlloc來得到的 返回值: 呼叫成功,返回指向該物件的第一個位元組的指標 呼叫失敗,返回NULL,可以用GetLastError來獲得出錯資訊 注意: 呼叫過GlobalLock鎖定一塊記憶體區後,一定要呼叫GlobalUnlock來解鎖 GlobalUnlock 函式功能描述:解除被鎖定的全域性記憶體物件 函式原型:BOOL GlobalUnlock( HGLOBAL hMem ); 引數:hMem:全域性記憶體物件的控制代碼 返回值: 非零值,指定的記憶體物件仍處於被鎖定狀態 0,函式執行出錯,可以用GetLastError來獲得出錯資訊,如果返回NO_ERROR,則表示記憶體物件已經解鎖了 注意:
這個函式實際上是將記憶體物件的鎖定計數器減一,如果計數器不為0,則表示執行過多個GlobalLock函式來對這個記憶體物件加鎖,需要對應數目的GlobalUnlock函式來解鎖。如果通過GetLastError函式返回錯誤碼為ERROR_NOT_LOCKED,則表示未加鎖或已經解鎖。 示例: // Malloc memory hMem = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, nSize); 分配方式 // Lock memory pMem = (BYTE *) GlobalLock(hMem); ……………… // Unlock memory GlobalUnlock(hMem); GlobalFree(hMem);
三 總結 靈活自由是C/C++語言的一大特色,而這也為C/C++程式設計師出了一個難題。當程式越來越複雜時,記憶體的管理也 會變得越加複雜,稍有不慎就會出現記憶體問 題。記憶體洩漏是最常見的記憶體問題之一。記憶體洩漏如果不是很嚴重 ,在短時間內對程式不會有太大的影響,這也使得記憶體洩漏問題有很強的隱蔽性,不容易被發現。 然而不管內 存洩漏多麼輕微,當程式長時間執行時,其破壞力是驚人的,從效能下降到記憶體耗盡,甚至會影響到其他程式 的正常執行。另外記憶體問題的一個共同特點 是,記憶體問題本身並不會有很明顯的現象,當有異常現象出現時已 時過境遷,其現場已非出現問題時的現場了,這給除錯記憶體問題帶來了很大的難度。