1. 程式人生 > >C語言動態記憶體分配:(一)malloc/free的實現及malloc實際分配/釋放的記憶體

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++安全編碼》