C語言動態記憶體分配:(一)malloc/free的實現及malloc實際分配/釋放的記憶體
一、malloc/free概述
malloc是在C語言中用於在程式執行時在堆中進行動態記憶體分配的庫函式。free是進行記憶體釋放的庫函式。
1、函式原型
#include <stdlib.h>
void *malloc(
size_t size
);
void free(
void* memblock
);
2、返回值
成功時,返回所分配儲存空間的起始地址;返回值型別為void*,在C語言中可以把void*直接付給具體的型別,但是在C++中必須進行強制型別轉換
失敗時(記憶體不足時)返回NULL
size為0時,返回的指標不是NULL;但是除了free,別的地方不要使用這個指標。
3、使用示例
#include <stdlib.h> /* For _MAX_PATH definition */ #include <stdio.h> #include <malloc.h> void main( void ) { char *string; /* Allocate space for a path name */ string = malloc( _MAX_PATH ); // In a C++ file, explicitly cast malloc's return. For example, // string = (char *)malloc( _MAX_PATH ); if( string == NULL ) printf( "Insufficient memory available\n" ); else { printf( "Memory space allocated for path name\n" ); free( string ); printf( "Memory freed\n" ); } }
二、malloc實際分配的記憶體大小
malloc實際分配的記憶體會大於我們需要的size。主要由兩方面因素決定:
1、位元組對齊。會對齊到機器最受限的型別(具體的實現因機器而異)。
2、“塊頭部資訊”。每個空閒塊都有“頭部”控制資訊,其中包含一個指向連結串列中下一個塊的指標、當前塊的大小和一個指向本身的指標。為了簡化塊對齊,所有塊的大小都必須是頭部大小的整數倍,且頭部已正確對齊。
在VC平臺下由_CrtMemBlockHeader結構體實現。
以下為《C程式設計語言》中給出的通過union進行的頭部實現,其中假定機器的受限型別為long。
說明:typedef long Align;/*按照long型別的邊界對齊*/ union header/*塊的頭部*/ { struct { union header *ptr;/*空閒塊連結串列中的下一塊*/ unsigned size;/*本塊的大小*/ }s; Align x;/*強制塊對齊*/ };
(1)實際分配的記憶體塊將多一個單元,用於頭部本身。實際分配的塊的大小被記錄在頭部的size欄位中。
(2)size欄位是必須的,因為malloc函式控制的塊不一定是連續的,這樣就不能通過指標算術運算計算其大小。
(3)malloc返回的是空閒塊的首地址,而不是首地址。
三、malloc/free實現過程
1、空閒儲存空間以空閒連結串列的方式組織(地址遞增),每個塊包含一個長度、一個指向下一塊的指標以及一個指向自身儲存空間的指標。( 因為程式中的某些地方可能不通過malloc呼叫申請,因此malloc管理的空間不一定連續。)
2、當有申請請求時,malloc會掃描空閒連結串列,直到找到一個足夠大的塊為止(首次適應)(因此每次呼叫malloc時並不是花費了完全相同的時間)。
3、如果該塊恰好與請求的大小相符,則將其從連結串列中移走並返回給使用者。如果該塊太大,則將其分為兩部分,尾部的部分分給使用者,剩下的部分留在空閒連結串列中(更改頭部資訊)。因此malloc分配的是一塊連續的記憶體。
4、釋放時,首先搜尋空閒連結串列,找到可以插入被釋放塊的合適位置。如果與被釋放塊相鄰的任一邊是一個空閒塊,則將這兩個塊合為一個更大的塊,以減少記憶體碎片。
四、實現
以下為《C語言程式設計語言》中給出的一種實現方法
1、malloc的實現
typedef union header Header;
static Header base;/*從空連結串列開始*/
static Header *freep = NULL;/*空閒連結串列的初始指標*/
void *malloc(unsigned nbytes)
{
Header *p, *prevp;
Header *morecore(unsigned);
unsigned nunits;
nunits = (nbytes+sizeof(Header)-1)/sizeof(Header) + 1;
if((prevp = freep) == NULL) /* 沒有空閒連結串列 */
{
base.s.ptr = freep = prevp = &base;
base.s.size = 0;
}
for(p = prevp->s.ptr; ;prevp = p, p= p->s.ptr)
{
if(p->s.size >= nunits) /* 足夠大 */
{
if (p->s.size == nunits) /* 正好 */
prevp->s.ptr = p->s.ptr;
else /*分配末尾部分*/
{
p->s.size -= nunits;
p += p->s.size;
p->s.size = nunits;
}
freep = prevp;
return (void*)(p+1);
}
if (p== freep) /* 閉環的空閒連結串列*/
if ((p = morecore(nunits)) == NULL)
return NULL; /* 沒有剩餘的儲存空間 */
}
}
說明:
(1)malloc實際分配的空間是Header大小的整數倍,並且多出一個Header空間用於放置Header
(2)式nunits = (nbytes+sizeof(Header)-1)/sizeof(Header) + 1中的減1是為了防止(nbytes+sizeof(Header))%sizeof(Header) == 0時,多分配了一個Header大小的空間
(3)第一次呼叫malloc函式時,freep為NULL,系統將建立一個退化的空閒連結串列,它只包含一個大小為0的塊,且該塊指向自己。任何時候,當請求空閒空間時,都將搜尋空閒塊連結串列。搜尋從上一次找到空閒塊的地方(freep)開始。該策略可以保證連結串列是均勻的。如果找到的塊太大,則將其尾部返回給使用者,這樣,初始塊的頭部只需要修改size欄位即可。
(4)任何時候,返回給使用者的指標都指向塊內的空閒儲存空間,即比指向頭部的指標大一個單元。
(5)sbrk不是系統呼叫,是C庫函式。sbrk/brk是從堆中分配空間,本質是移動一個位置,向後移就是分配空間,向前移就是釋放空間,sbrk用相對的整數值確定位置,如果這個整數是正數,會從當前位置向後移若干位元組,如果為負數就向前若干位元組。在任何情況下,返回值永遠是移動之前的位置。在LINUX中sbrk(0)能返回比較精確的虛擬記憶體使用情況。連結:sbrk()/brk()--改變資料長度
2、morecore的實現
函式morecore用來向作業系統請求儲存空間,其實現細節因系統的不同而不同。因為向系統請求儲存空間是一個開銷很大的操作,因此我們不希望每次呼叫malloc時都執行該操作,基於這個考慮,morecore函式請求至少NALLOC個單元。這個較大的塊將根據需要分成較小的塊。在設定完size欄位後,morecore函式呼叫free函式把多餘的儲存空間插入到空閒區域中。
#define NALLOC 1024 /* 最小申請單元數 */
static Header *morecore(unsigned nu)
{
char *cp;
Header *up;
if(nu < NALLOC)
nu = NALLOC;
cp = sbrk(nu * sizeof(Header));
if(cp == (char *)-1) /* 沒有空間*/
return NULL;
up = (Header *)cp;
up->s.size = nu;
free((void *)(up+1));
return freep;
}
說明:
(1)沒有儲存空間時sbrk呼叫返回-1.因此需要將-1強制型別轉換為char*型別,以便 與返回值進行比較。而且,強制型別轉換使得函式不會受不同機器中指標表示的不同的影響。
3、free的實現
free函式從freep指向的地址開始,逐個掃描空閒連結串列,尋找可以插入空閒塊的地方。該位置可能在連結串列的末尾或者兩個空閒塊之間。在任何一種情況下,如果被釋放的塊與另一空閒塊相鄰,則將這兩個塊合併。
void free(void *ap)
{
Header *bp,*p;
bp = (Header *)ap -1; /* 指向塊頭 */
for(p=freep;!(bp>p && bp< p->s.ptr);p=p->s.ptr)
if(p>=p->s.ptr && (bp>p || bp<p->s.ptr))
break; /* 被釋放的塊在連結串列的開頭或結尾*/
if (bp+bp->s.size==p->s.ptr) /*與上一相鄰塊合併 */
{
bp->s.size += p->s.ptr->s.size;
bp->s.ptr = p->s.ptr->s.ptr;
}
else
bp->s.ptr = p->s.ptr;
if (p+p->s.size == bp)/* 與下一相鄰塊合併 */
{
p->s.size += bp->s.size;
p->s.ptr = bp->s.ptr;
}
else
p->s.ptr = bp;
freep = p;
}
五、注意事項
參考文獻:
1、《C程式設計語言》
2、《C和指標》
3、《C和C++安全編碼》