C++內存分配詳解

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 <malloc.h>或#i nclude <stdlib.h>功能:分配長度爲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,GlobalFree調用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++程序員出了一個難題。當程序越來越複雜時,內存的管理也會變得越加複雜,稍有不慎就會出現內存問 題。內存泄漏是最常見的內存問題之一。內存泄漏如果不是很嚴重,在短時間內對程序不會有太大的影響,這也使得內存泄漏問題有很強的隱蔽性,不容易被發現。 然而不管內存泄漏多麼輕微,當程序長時間運行時,其破壞力是驚人的,從性能下降到內存耗盡,甚至會影響到其他程序的正常運行。另外內存問題的一個共同特點 是,內存問題本身並不會有很明顯的現象,當有異常現象出現時已時過境遷,其現場已非出現問題時的現場了,這給調試內存問題帶來了很大的難度。


下載Windows Debug 工具安裝後,使用其中的gflags.exe工具打開PageHeap,gflags -p /enable MainD.exe /full重新使用VS用調試方式運行,很快就找到了出錯位置,因爲在某個靜態函數中筆誤導致。在編寫穩定的服務器程序時,這個工具尤爲有用。

1. 首先我們來看HeapAlloc:MSDN上的解釋爲:HeapALloc是從堆上分配一塊內存,且分配的內存是不可移動的(即如果沒有連續的空間能滿足分配的大小,程序不能將其他零散的空間利用起來,從而導致分配失敗),該分配方法是從一指定地址開始分配,而不像GloabalAlloc是從全局堆上分配,這個有可能是全局,也有可能是 局部。函數原型爲:LPVOID HeapAlloc(HANDLE hHeap,DWORD dwFlags,SIZE_T dwBytes);hHeap是進程堆內存開始位置。


dwFlags是分配堆內存的標誌。包括HEAP_ZERO_MEMORY,即使分配的空間清零。dwBytes是分配堆內存的大小。其對應的釋放空間函數爲HeapFree.

2. 再看GlobalAlloc:該函數用於從全局堆中分配出內存供程序使用,函數原型爲:HGLOBAL GlobalAlloc(UINT uFlags,SIZE_T dwBytes);uFlags參數含義GHND GMEM_MOVEABLE和GMEM_ZEROINIT的組合GMEM_FIXED 分配固定內存,返回值是一個指針GMEM_MOVEABLE 分配活動內存,在Win32中,內存塊不能在物理內存中移動,但能在默認的堆中移動。返回值是內存對象的句柄,用函數GlobalLock可將句柄轉化爲指針GMEM_ZEROINIT 將內存內容初始化爲零GPTR GMEM_FIXED和GMEM_ZEROINIT的組合一般情況下我們在編程的時候,給應用程序分配的內存都是可以移動的或者是可以丟棄的,這樣能使有限的內存資源充分利用,所以,在某一個時候我們分配的那塊內存的地址是不確定的,因爲他是可以移動的,所以得先鎖定那塊內存塊,這兒應用程序需要調用API函數GlobalLock函數來鎖定句柄。如下: lpMem=GlobalLock(hMem); 這樣應用程序才能存取這塊內存。所以我們在使用GlobalAllock時,通常搭配使用GlobalLock,當然在不使用內存時,一定記得使用 GlobalUnlock,否則被鎖定的內存塊一直不能被其他變量使用。


GlobalAlloc對應的釋放空間的函數爲GlobalFree.


3. LocalAlloc:該函數用於從局部堆中分配內存供程序使用,函數原型爲:HLOCAL LocalAlloc(UINT uFlags,SIZE_T uBytes);參數同GlobalAlloc.在16位Windows中是有區別的,因爲在16位windows用一個全局堆和局部堆來管理內存,每一個應用程序或dll裝入內存時,代碼段被裝入全局 堆,而系統又爲每個實例從全局堆中分配了一個64kb的數據段作爲該實例的局部堆,用來存放應用程序的堆棧和所有全局或靜態變量。而 LocalAlloc/GlobalAlloc就是分別用於在局部堆或全局堆中分配內存。


由於每個進程的局部堆很小,所以在局部堆中分配內存會受到空間的限制。但這個堆是每個進程私有的,相對而言分配數據較安全,數據訪問出錯不至於影響到整個系統。而在全局堆中分配的內存是爲各個進程共享的,每個進程只要擁有這個內存塊的句柄都可以訪問這塊內存,但是每個全局內存空間需要額外的內存開銷,造成分配浪費。而且一旦發生嚴重錯誤,可能會影響到整個系統的穩定。


不過在Win32中,每個進程都只擁有一個省缺的私有堆,它只能被當前進程訪問。應用程序也不可能直接訪問系統內存。所以在Win32中全局堆和局部堆都指向進程的省缺堆。用LocalAlloc/GlobalAlloc分配內存沒有任何區別。甚至LocalAlloc分配的內存可以被 GlobalFree釋放掉。所以在Win32下編程,無需注意Local和Global的區別,一般的內存分配都等效於 HeapAlloc(GetProcessHeap(),……)。


LocalAlloc對應的釋放函數爲LockFree.


4. VirtualAlloc:該函數的功能是在調用進程的虛地址空間,預定或者提交一部分頁,如果用於內存分配的話,並且分配類型未指定MEM_RESET,則系統將自動設置爲0;其函數原型:LPVOID VirtualAlloc(LPVOID lpAddress, // region to reserve or commit SIZE_T dwSize, // size of region DWORD flAllocationType, // type of allocation DWORD flProtect // type of access protection);VirtualAlloc可以通過並行多次調用提交一個區域的部分或全部來保留一個大的內存區域。多重調用提交同一塊區域不會引起失敗。這使得一個應用程 序保留內存後可以隨意提交將被寫的頁。當這種方式不在有效的時候,它會釋放應用程序通過檢測被保留頁的狀態看它是否在提交調用之前已經被提交。


VirtualAlloc對應的釋放函數爲VirtualFree.


5.Malloc:malloc與free是C++/C語言的標準庫函數,可用於申請動態內存和釋放內存。對於非內部數據類型的對象而言,光用 malloc/free無法滿足動態對象的要求。對象在創建的同時要自動執行構造函數,對象在消亡之前要自動執行析構函數。由於malloc/free是 庫函數而不是運算符,不在編譯器控制權限之內,不能夠把執行構造函數和析構函數的任務強加於malloc/free.


6.New:new/delete是C++的運算符。可用於申請動態內存和釋放內存。C++語言需要一個能完成動態內存分配和初始化工作的運算符new, 以一個能完成清理與釋放內存工作的運算符delete.注意new/delete不是庫函數。C++程序經常要調用C函數,而C程序只能用malloc /free管理動態內存。new 是個操作符,和什麼“+”,“-”,“=”……有一樣的地位。


malloc是個分配內存的函數,供你調用的。


new是保留字,不需要頭文件支持。


malloc需要頭文件庫函數支持。new 建立的是一個對象,malloc分配的是一塊內存。


new建立的對象你可以把它當成一個普通的對象,用成員函數訪問,不要直接訪問它的地址空間malloc分配的是一塊內存區域,就用指針訪問好了,而且還可以在裏面移動指針。


內存泄漏對於malloc或者new都可以檢查出來的,區別在於new可以指明是那個文件的那一行,而malloc沒有這些信息。new可以認爲是malloc加構造函數的執行。new出來的指針是直接帶類型信息的。而malloc返回的都是void指針。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章