malloc的内存分配原理
0 堆內(nèi)存的在計算機內(nèi)存中的形式
根據(jù)《The?C Programming language》推測得到堆內(nèi)存,圖中的Heap區(qū)域即為堆內(nèi)存塊(Heap區(qū)域的數(shù)目不代表計算機堆內(nèi)存的真實數(shù)目)。
?
[1]?堆內(nèi)存不連續(xù)。只有標識為Heap的才是堆內(nèi)存。
[2] ?在malloc()/free()看來,每個Heap所代表的的堆由兩部分組成:Header +可給用戶使用的堆內(nèi)存。在Header中包含了“指向下一鄰近高地址堆內(nèi)存塊的指針”、“本堆塊的大小”。每次由malloc()函數(shù)分配給用戶的堆內(nèi)存也必須包含Header結構(且所占內(nèi)存就在返回給用戶使用的堆內(nèi)存之前),這樣是為了讓malloc()/free()更好的管理堆內(nèi)存。
[3]?malloc()/free()函數(shù)操作的堆內(nèi)存是如圖所示的一個鏈(Heap1 -> Heap2 ->Heap3 ->Heap4 ->Heap1),可通過此鏈表訪問到任意一段堆內(nèi)存。所以,經(jīng)malloc()函數(shù)實際分配得到的堆內(nèi)存要比用戶實際需求的要大一個Header,只是返回給用戶的堆內(nèi)存大小剛好是用戶所需。free()釋放時,也要根據(jù)Header的內(nèi)容將此段曾供給用戶使用過得堆內(nèi)存釋放到最鄰近的一個堆塊中去。
?
?
這就是內(nèi)存中的堆內(nèi)存。堆內(nèi)存由用戶用代碼分配及回收。堆和棧的區(qū)別不僅在于內(nèi)存的存在形式,在使用時棧一般擁有內(nèi)存名即棧內(nèi)存可以由內(nèi)存名(變量名)直接訪問,也可以通過地址(指針)訪問棧內(nèi)存。但對于堆內(nèi)存來說,堆不存在內(nèi)存名,只有通過地址(指針)訪問。
?
1堆內(nèi)存
Figure1:內(nèi)存中的堆內(nèi)存空間假設從《The ?C ?Programming ?Language》中推測正確,從未經(jīng)動態(tài)分配的堆內(nèi)存呈現(xiàn)上圖形式。不連續(xù)的堆內(nèi)存以“鏈”的形式聯(lián)系:Heap1 -> Heap2 ->Heap3 ->Heap4->Heap1。筆跡將構成“堆鏈”的每個堆內(nèi)存(如Heap1)稱為“堆塊”。malloc()/free()將每個堆塊看作由兩部分構成:“Header”和“可用堆內(nèi)存”。在Header中包含了“指向下一個堆內(nèi)存塊的指針”、“本堆塊的大小”。這樣malloc()/free()就能更好地管理堆。
2 堆內(nèi)存分配
[1] mallco()分配機制
根據(jù)C中malloc(n)函數(shù)動態(tài)分配堆的機制:分配堆內(nèi)存的時候就依序由低到高的地址搜索“堆鏈”中的堆塊,搜索到“可用堆內(nèi)存”滿足n的堆塊(如Heap1)為止。若Heap1的“可用堆內(nèi)存”剛好滿足n,則將Heap1從“堆鏈”中刪除,同時重新組織各堆塊形成新的“堆鏈”;若Heap1的“可用堆內(nèi)存”大小大于n,則將malloc(n)申請到的“Header” + "可用堆內(nèi)存"部分從Heap1中分裂,將剩余的Heap1堆內(nèi)存塊重新加入“堆鏈”中。經(jīng)分裂后的堆內(nèi)存也包含“Header”和“可用堆內(nèi)存”兩部分(如圖Figure 2),然后將由malloc()分配得到的“可用堆內(nèi)存”返回給用戶。若某塊堆內(nèi)存空間比較大(如Heap1),能夠滿足較小內(nèi)存的多次申請,那么由malloc(n)多次申請的堆內(nèi)存塊都是連續(xù)被分配給用戶的(因為有Header,所以用戶使用的堆地址不連續(xù))。 為方便free()釋放堆空間,經(jīng)malloc(n)分配給用戶的堆空間也隱含一個Header。如下圖所示: Figure2:malloc()分配的堆內(nèi)存結構由于Header的構成的內(nèi)存對齊,C中malloc(n)函數(shù)分配的堆內(nèi)存會大于等于Header + n。
3 malloc()分配內(nèi)存
可先參見位經(jīng)malloc()函數(shù)申請分配的堆內(nèi)存在計算機中的形式:計算機中的堆。
?
經(jīng)malloc()分配過得堆內(nèi)存結構如下:
Read From《The?C?Programming?Language》。
?
可用的堆內(nèi)存塊以“可用堆內(nèi)存鏈表”的形式存在。malloc()進行動態(tài)分配的特點:
- malloc()根據(jù)用戶所需分配內(nèi)存的大小n (bytes)在“堆鏈表”(見未使用過得堆內(nèi)存)里搜索。直到搜索到一個大于等于n字節(jié)的堆內(nèi)存塊為止。如果此堆內(nèi)存塊的大小剛好為n,則直接將首地址返回給用戶;如果此內(nèi)存塊的大小大于n,則將此塊堆內(nèi)存分裂,將大于n部分的堆內(nèi)存留在可用堆內(nèi)存中,以“堆鏈表”的形式和其它未分配的堆內(nèi)存發(fā)生聯(lián)系。
-
如果整個堆鏈表所代表的堆內(nèi)存塊都沒有大于等于n的堆內(nèi)存塊,系統(tǒng)將給“堆鏈表”鏈接一個更大的區(qū)域供其使用。要是這一步也失敗了,malloc()函數(shù)就返回NULL給用戶。
malloc()函數(shù)分配內(nèi)存成功則返回可用堆內(nèi)存塊的首地址,若分配失敗則返回空。在使用malloc()后一定要判斷堆內(nèi)存是否成功。若對內(nèi)存分配未成功使用指針操作內(nèi)存也會使程序出現(xiàn)異常。動態(tài)分配內(nèi)存時要采取以下結構:
?
[cpp]?view plaincopyprint??
分配成功后,得到的堆內(nèi)存首地址一定要保存,不然后來無法釋放堆內(nèi)存而造成內(nèi)存泄露。而且不可使用未初始化的pL指向的內(nèi)存塊。
?
4 用指針來使用堆空間
- 定義指針后,釋放堆空間后都應將指針賦值為NULL。若指針之上有地址值,而以此地址值為起始地址的內(nèi)存空間不再可用,則就形成了野指針,野指針有潛在的危險。
- 在上一點的基礎之上,使用指針前判斷其值是否為NULL。
- 以指針為索引(堆內(nèi)存無名),若malloc分配內(nèi)存成功,初始化堆內(nèi)存(malloc時,大小要不為0)。malloc前的強制轉換類型規(guī)定了申請的堆內(nèi)存將要存的數(shù)據(jù)類型。
- free堆內(nèi)存后,指針保存的地址值還在,只是那塊內(nèi)存已經(jīng)被回收了,所以需要再次將指針的值設為NULL,避免使用野指針。free內(nèi)存時,按照邏輯來,防止內(nèi)存泄露。
?
指針名所代表的4 bytes內(nèi)存上存了堆內(nèi)存的首地址后,訪問這塊堆內(nèi)存內(nèi)容跟平時使用指針差不多。可以以指針的形式訪問(甚用p++ || ++p,堆內(nèi)存首地址可不要丟失,留著釋放),也可以使用下標的形式訪問。
5 free()內(nèi)存
當使用free()函數(shù)釋放堆內(nèi)存的時候,free()函數(shù)將堆內(nèi)存插入到于要釋放堆內(nèi)存地址最鄰近的一個位置上,盡可能的使堆內(nèi)存以大塊的形式存在而不至于讓堆內(nèi)存稱為碎片。
?
釋放未指向任何堆內(nèi)存塊的指針也會造成內(nèi)存泄露。所以在釋放指針前的一個基本操作是判斷指針內(nèi)容是否為空,free(p)后只是將p指向的內(nèi)存回收,p的值依舊存在,為避免再次使用p的值還需要將p賦值為NULL(因為使用指針前都會判斷是否為NULL)。釋放堆內(nèi)存塊采取這樣的程序結構:
?
[cpp]?view plaincopyprint?6 指針賦值為NULL的道理
有筆記“C中的void和NULL”表面引用NULL指針的后果。為了更好的利用指針,避免野指針(指針所指的內(nèi)存塊不可用)的使用在所有使用指向堆內(nèi)存塊的指針前都采取如此的結構:
?
[cpp]?view plaincopyprint?定義指針后將其值賦值為NULL。此時指針指向的內(nèi)存地址為NULL,NULL對指針的賦值是將指針置成空指針(什么也沒有指向)還是將指針指向了一段特殊的地址取決于編譯器,編程中我們不需要了解NULL到底代表什么,只需要用NULL來避免指針帶來的后果。
?
定義指針后將其賦值為NULL之后的好處在于避免系統(tǒng)給予局部指針變量的隨機值,我們在使用指針前(malloc()除外)都判斷一下指針的值是否為NULL,只有在不為空的情況下才能對此進行操作,如free(p),若在不判斷p是否為空的情況下進行free(p)操作則會造成內(nèi)存泄露。
7 在含指針參數(shù)的函數(shù)內(nèi)使用斷言
(1)用斷言判斷指針是否為NULL
判斷指針是否為NULL的主要針對對象是指向堆內(nèi)存的指針。比如在以下內(nèi)存拷貝函數(shù)中:
?
[cpp]?view plaincopyprint??
程序中首先判斷兩個地址是否為空。判斷StrTo是為了了解StrTo是否指向一段空間。當然若StrTo指向一個常數(shù),往后拷貝操作還得出錯。
?
像這樣帶指針參數(shù)的子函數(shù)內(nèi)都很有必要有這么一段判斷指針是否為NULL的語句,故而可以將這樣的代碼寫成函數(shù)來供大家使用,再考慮此代碼段比較小可以用宏代替。這樣的(帶參數(shù))宏可稱為斷言,因為當指針未空時就退出子函數(shù)(如assert())。
?
如以上一段判斷子函數(shù)是否為空可以用如下宏代替,形成一個斷言:
?
[cpp]?view plaincopyprint??
然后在每個程序中直接調(diào)用MY_ASSERT(pStrTo, pStrFrom);即可。由于這樣的宏(斷言)可能供許多函數(shù)的使用,所以一定要保證它的正確性。
?
(2)內(nèi)存塊重疊
內(nèi)存塊重疊指多個指針指向的內(nèi)存有重疊的情況。對內(nèi)存塊的操作是否會影響源內(nèi)存塊的內(nèi)容(如內(nèi)存數(shù)據(jù)拷貝)。
兩指針指向的內(nèi)存塊重疊
如上圖將p2指向內(nèi)存的數(shù)據(jù)拷貝給p1代表的內(nèi)存中去后,p2指向的內(nèi)存塊數(shù)據(jù)也被改變。堆內(nèi)存塊的操作不要有副作用。
?
8 總結
(1)用NULL(因其特殊性)來統(tǒng)一標識指針的可用性。使用指針前都應該判斷一下指針是否為NULL。
(2)將局部指針變量初始化為NULL(消除系統(tǒng)給其賦的隨機值,系統(tǒng)為其賦隨機值也就造就了野指針)。
(3)指針用于指向堆內(nèi)存時需要注意:
-
malloc()后一定要判斷是否malloc()成功。malloc()成功后一定要保存所分配堆內(nèi)存塊的首地址。
-
使用堆內(nèi)存塊前要初始化。
-
使用堆內(nèi)存塊不可越界。
-
正確釋放每個堆內(nèi)存塊。且釋放后將指針的值重新賦值為NULL。
(4)使用指針前都應該判斷一下指針是否為NULL。
?
?
完全使用完某個指針或釋放指向堆內(nèi)存的指針后,將其值賦值為空。指向堆內(nèi)存的指針在釋放完需要賦值為空的理由見free ()堆內(nèi)存。對于指針定義時初始化和完全使用完指針后再將其值賦為NULL的道理在于所有使用指針的語句前都會有有判斷指針是否為NULL的語句。尤其是在子函數(shù)內(nèi)判斷指向堆內(nèi)存塊的指針實參是否為NULL。
9 malloc()分配總結
對于C中的malloc(n)分配,有以下進一步的結論:
- 實際分配的堆內(nèi)存是Header + n結構。返回給用戶的是n部分的首地址。
- 由于內(nèi)存對齊值8,實際分配的堆內(nèi)存大于等于sizeof(Header) + n。
總結
以上是生活随笔為你收集整理的malloc的内存分配原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【ORACLE】 安装需要注意的问题(一
- 下一篇: js正则表达式test()和exec()