0 堆內存的在計算機內存中的形式
根據《The C Programming language》推測得到堆內存,圖中的Heap區域即為堆內存塊(Heap區域的數目不代表計算機堆內存的真實數目)。
[1] 堆內存不連續。只有標識為Heap的才是堆內存。
[2] 在malloc()/free()看來,每個Heap所代表的的堆由兩部分組成:Header +可給用戶使用的堆內存。在Header中包含了“指向下一鄰近高地址堆內存塊的指針”、“本堆塊的大小”。每次由malloc()函數分配給用戶的堆內存也必須包含Header結構(且所占內存就在返回給用戶使用的堆內存之前),這樣是為了讓malloc()/free()更好的管理堆內存。
[3] malloc()/free()函數操作的堆內存是如圖所示的一個鏈(Heap1 -> Heap2 ->Heap3 ->Heap4 ->Heap1),可通過此鏈表訪問到任意一段堆內存。所以,經malloc()函數實際分配得到的堆內存要比用戶實際需求的要大一個Header,只是返回給用戶的堆內存大小剛好是用戶所需。free()釋放時,也要根據Header的內容將此段曾供給用戶使用過得堆內存釋放到最鄰近的一個堆塊中去。
這就是內存中的堆內存。堆內存由用戶用代碼分配及回收。堆和棧的區別不僅在於內存的存在形式,在使用時棧一般擁有內存名即棧內存可以由內存名(變量名)直接訪問,也可以通過地址(指針)訪問棧內存。但對於堆內存來說,堆不存在內存名,只有通過地址(指針)訪問。
1堆內存

假設從《The C Programming Language》中推測正確,從未經動態分配的堆內存呈現上圖形式。不連續的堆內存以“鏈”的形式聯系:Heap1 -> Heap2 ->Heap3 ->Heap4->Heap1。筆跡將構成“堆鏈”的每個堆內存(如Heap1)稱為“堆塊”。malloc()/free()將每個堆塊看作由兩部分構成:“Header”和“可用堆內存”。在Header中包含了“指向下一個堆內存塊的指針”、“本堆塊的大小”。這樣malloc()/free()就能更好地管理堆。
2 堆內存分配
[1] mallco()分配機制
根據C中malloc(n)函數動態分配堆的機制:分配堆內存的時候就依序由低到高的地址搜索“堆鏈”中的堆塊,搜索到“可用堆內存”滿足n的堆塊(如Heap1)為止。若Heap1的“可用堆內存”剛好滿足n,則將Heap1從“堆鏈”中刪除,同時重新組織各堆塊形成新的“堆鏈”;若Heap1的“可用堆內存”大小大於n,則將malloc(n)申請到的“Header” + "可用堆內存"部分從Heap1中分裂,將剩余的Heap1堆內存塊重新加入“堆鏈”中。經分裂后的堆內存也包含“Header”和“可用堆內存”兩部分(如圖Figure 2),然后將由malloc()分配得到的“可用堆內存”返回給用戶。 若某塊堆內存空間比較大(如Heap1),能夠滿足較小內存的多次申請,那么由malloc(n)多次申請的堆內存塊都是連續被分配給用戶的(因為有Header,所以用戶使用的堆地址不連續)。
由於Header的構成的內存對齊,C中malloc(n)函數分配的堆內存會大於等於Header + n。
3 malloc()分配內存
可先參見位經malloc()函數申請分配的堆內存在計算機中的形式:計算機中的堆。
經malloc()分配過得堆內存結構如下:
Read From《The C Programming Language》。
可用的堆內存塊以“可用堆內存鏈表”的形式存在。malloc()進行動態分配的特點:
-
malloc()根據用戶所需分配內存的大小n (bytes)在“堆鏈表”(見未使用過得堆內存)里搜索。直到搜索到一個大於等於n字節的堆內存塊為止。如果此堆內存塊的大小剛好為n,則直接將首地址返回給用戶;如果此內存塊的大小大於n,則將此塊堆內存分裂,將大於n部分的堆內存留在可用堆內存中,以“堆鏈表”的形式和其它未分配的堆內存發生聯系。
-
如果整個堆鏈表所代表的堆內存塊都沒有大於等於n的堆內存塊,系統將給“堆鏈表”鏈接一個更大的區域供其使用。要是這一步也失敗了,malloc()函數就返回NULL給用戶。
malloc()函數分配內存成功則返回可用堆內存塊的首地址,若分配失敗則返回空。在使用malloc()后一定要判斷堆內存是否成功。若對內存分配未成功使用指針操作內存也會使程序出現異常。動態分配內存時要采取以下結構:
- char *pL =NULL;
- ……
- pL = (char *)malloc( sizeof(char) * size);
- if(pL)
- {
- …
- }
分配成功后,得到的堆內存首地址一定要保存,不然后來無法釋放堆內存而造成內存泄露。而且不可使用未初始化的pL指向的內存塊。
4 用指針來使用堆空間
- 定義指針后,釋放堆空間后都應將指針賦值為NULL。若指針之上有地址值,而以此地址值為起始地址的內存空間不再可用,則就形成了野指針,野指針有潛在的危險。
- 在上一點的基礎之上,使用指針前判斷其值是否為NULL。
- 以指針為索引(堆內存無名),若malloc分配內存成功,初始化堆內存(malloc時,大小要不為0)。malloc前的強制轉換類型規定了申請的堆內存將要存的數據類型。
- free堆內存后,指針保存的地址值還在,只是那塊內存已經被回收了,所以需要再次將指針的值設為NULL,避免使用野指針。free內存時,按照邏輯來,防止內存泄露。
指針名所代表的4 bytes內存上存了堆內存的首地址后,訪問這塊堆內存內容跟平時使用指針差不多。可以以指針的形式訪問(甚用p++ || ++p,堆內存首地址可不要丟失,留着釋放),也可以使用下標的形式訪問。
5 free()內存
當使用free()函數釋放堆內存的時候,free()函數將堆內存插入到於要釋放堆內存地址最鄰近的一個位置上,盡可能的使堆內存以大塊的形式存在而不至於讓堆內存稱為碎片。
釋放未指向任何堆內存塊的指針也會造成內存泄露。所以在釋放指針前的一個基本操作是判斷指針內容是否為空,free(p)后只是將p指向的內存回收,p的值依舊存在,為避免再次使用p的值還需要將p賦值為NULL(因為使用指針前都會判斷是否為NULL)。釋放堆內存塊采取這樣的程序結構:
- if(pL)
- {
- free(pL);
- pL = NULL;
- }
6 指針賦值為NULL的道理
有筆記“C中的void和NULL”表面引用NULL指針的后果。為了更好的利用指針,避免野指針(指針所指的內存塊不可用)的使用在所有使用指向堆內存塊的指針前都采取如此的結構:
- if(p)
- {
- //通過指針操作堆內存
- ……
- }
定義指針后將其值賦值為NULL。此時指針指向的內存地址為NULL,NULL對指針的賦值是將指針置成空指針(什么也沒有指向)還是將指針指向了一段特殊的地址取決於編譯器,編程中我們不需要了解NULL到底代表什么,只需要用NULL來避免指針帶來的后果。
定義指針后將其賦值為NULL之后的好處在於避免系統給予局部指針變量的隨機值,我們在使用指針前(malloc()除外)都判斷一下指針的值是否為NULL,只有在不為空的情況下才能對此進行操作,如free(p),若在不判斷p是否為空的情況下進行free(p)操作則會造成內存泄露。
7 在含指針參數的函數內使用斷言
(1)用斷言判斷指針是否為NULL
判斷指針是否為NULL的主要針對對象是指向堆內存的指針。比如在以下內存拷貝函數中:
- flag my_strcpy(char *StrTo, char *StrFrom)
- {
- if(!StrTo || !StrFrom)
- {
- return -1;
- }
- char *StrToL, *StrFromL;
- StrToL = StrTo;
- StrFromL = StrFrom;
- while(*StrToL++ = * StrFromL++)
- NULL;
- return 0;
- }
程序中首先判斷兩個地址是否為空。判斷StrTo是為了了解StrTo是否指向一段空間。當然若StrTo指向一個常數,往后拷貝操作還得出錯。
像這樣帶指針參數的子函數內都很有必要有這么一段判斷指針是否為NULL的語句,故而可以將這樣的代碼寫成函數來供大家使用,再考慮此代碼段比較小可以用宏代替。這樣的(帶參數)宏可稱為斷言,因為當指針未空時就退出子函數(如assert())。
如以上一段判斷子函數是否為空可以用如下宏代替,形成一個斷言:
- #define MY_ASSERT(pStrTo, pStrFrom) if(!StrTo || !StrFrom) \
- { \
- return -1; \
- }
然后在每個程序中直接調用MY_ASSERT(pStrTo, pStrFrom);即可。由於這樣的宏(斷言)可能供許多函數的使用,所以一定要保證它的正確性。
(2)內存塊重疊
內存塊重疊指多個指針指向的內存有重疊的情況。對內存塊的操作是否會影響源內存塊的內容(如內存數據拷貝)。
兩指針指向的內存塊重疊
如上圖將p2指向內存的數據拷貝給p1代表的內存中去后,p2指向的內存塊數據也被改變。堆內存塊的操作不要有副作用。
8 總結
(1)用NULL(因其特殊性)來統一標識指針的可用性。使用指針前都應該判斷一下指針是否為NULL。
(2)將局部指針變量初始化為NULL(消除系統給其賦的隨機值,系統為其賦隨機值也就造就了野指針)。
(3)指針用於指向堆內存時需要注意:
-
malloc()后一定要判斷是否malloc()成功。malloc()成功后一定要保存所分配堆內存塊的首地址。
-
使用堆內存塊前要初始化。
-
使用堆內存塊不可越界。
-
正確釋放每個堆內存塊。且釋放后將指針的值重新賦值為NULL。
(4)使用指針前都應該判斷一下指針是否為NULL。
完全使用完某個指針或釋放指向堆內存的指針后,將其值賦值為空。指向堆內存的指針在釋放完需要賦值為空的理由見free ()堆內存。對於指針定義時初始化和完全使用完指針后再將其值賦為NULL的道理在於所有使用指針的語句前都會有有判斷指針是否為NULL的語句。尤其是在子函數內判斷指向堆內存塊的指針實參是否為NULL。
9 malloc()分配總結
對於C中的malloc(n)分配,有以下進一步的結論:
- 實際分配的堆內存是Header + n結構。返回給用戶的是n部分的首地址。
- 由於內存對齊值8,實際分配的堆內存大於等於sizeof(Header) + n。