malloc()是C語言中動態存儲管理 的一組標準庫函數之一。其作用是在內存的動態存儲區中分配一個長度爲size的連續空間。其參數是一個無符號整形數,返回值 是一個指向所分配的連續存儲域的起始地址的指針。
動態內存分配 就 是指在程序執行的過程中動態地分配或者回收存儲空間的分配內存的方法。動態內存分配不像數組等靜態內存分配方法那樣需要預先分配存儲空間,而是由系統根據 程序的需要即時分配,且分配的大小就是程序要求的大小。本文簡單介紹動態內存分配函數malloc()及幾種實現方法。
1. 簡介
malloc()是C語言中動態存儲管理的一組標準庫函數之一。其作用是在內存的動態存儲區中分配一個長度爲size的連續空間。其參數是一個無符號整形數,返回值是一個指向所分配的連續存儲域的起始地址的指針。還有一點必須注意的是,當函數未能成功分配存儲空間(如內存不足 )就會返回一個NULL指針。所以在調用該函數時應該檢測返回值是否爲NULL並執行相應的操作。
2. 函數說明
C語言的動態存儲管理由一組標準庫函數實現,其原型在標準文件<stdlib.h>裏描述,需要用這些功能時應包含這個文件。與動態存儲分 配有關的函數共有四個,其中就包括存儲分配函數malloc()。函數原型是:void *malloc (size_t n);這裏的size_t是標準庫裏定義的一個類型,它是一個無符號整型。這個整型能夠滿足所有對存儲塊大小描述的需要,具體相當於哪個整型由具體的C系 統確定。malloc的返回值爲(void *)類型(這是通用指針的一個重要用途),它分配一片能存放大小爲n的數據的存儲塊,返回對應的指針值;如果不能滿足申請(找不到能滿足要求的存儲塊)就
返回NULL。在使用時,應該把malloc的返回值轉換到特定指針類型,賦給一個指針。
注意,雖然這裏的存儲塊是通過動態分配得到的,但 是它的大小也是確定的,同樣不允許越界使用。例如上面程序段分配的塊裏能存n個雙精度數據,隨後的使用就必須在這個範圍內進行。越界使用動態分配的存儲 塊,尤其是越界賦值,可能引起非常嚴重的後果,通常會破壞程序的運行系統,可能造成本程序或者整個計算機系統 垮臺。
下例是一個動態分配的例子:
#include <stdlib.h>
main()
{
int count,*array; /*count是一個計數器,array是一個整型指針,也可以理解爲指向一個整型數組的首地址*/
if((array(int *) malloc (10*sizeof (int)))==NULL)
{
printf("不能成功分配存儲空間。");
exit(1);
}
for (count=0;count〈10;count++) /*給數組賦值*/
array[count]=count;
for(count=0;count〈10;count++) /*打印數組元素*/
printf("%2d",array[count]);
}
上例中動態分配了10個整型存儲區域,然後進行賦值並打印。例中if((array(int *) malloc (10*sizeof(int)))==NULL)語句可以分爲以下幾步:
1)分配10個整型的連續存儲空間,並返回一個指向其起始地址的整型指針
2)把此整型指針地址賦給array
3)檢測返回值是否爲NULL
3. malloc()工作機制
malloc函數的實質體現在,它有一個將可用的內存塊連接爲一個長長的列表的所謂空閒鏈表。調用malloc函數時,它沿連接表尋找一個大到足以滿足 用戶請求所需要的內存塊。然後,將該內存塊一分爲二(一塊的大小與用戶請求的大小相等,另一塊的大小就是剩下的字節)。接下來,將分配給用戶的那塊內存傳 給用戶,並將剩下的那塊(如果有的話)返回到連接表上。調用free函數時,它將用戶釋放的內存塊連接到空閒鏈上。到最後,空閒鏈會被切成很多的小內存片 段,如果這時用戶申請一個大的內存片段,那麼空閒鏈上可能沒有可以滿足用戶要求的片段了。於是,malloc函數請求延時,並開始在空閒鏈上翻箱倒櫃地檢
查各內存片段,對它們進行整理,將相鄰的小空閒塊合併成較大的內存塊。
4. malloc()在操作系統中的實現
在 C 程序中,多次使用malloc () 和 free()。不過,您可能沒有用一些時間去思考它們在您的操作系統中是如何實現的。本節將向您展示 malloc 和 free 的一個最簡化實現的代碼,來幫助說明管理內存時都涉及到了哪些事情。
在大部分操作系統中,內存分配由以下兩個簡單的函數來處理:
void *malloc (long numbytes):該函數負責分配 numbytes 大小的內存,並返回指向第一個字節的指針。
void free(void *firstbyte):如果給定一個由先前的 malloc 返回的指針,那麼該函數會將分配的空間歸還給進程的“空閒空間”。
malloc_init 將是初始化內存分配程序的函數。它要完成以下三件事:將分配程序標識爲已經初始化,找到系統中最後一個有效內存地址 ,然後建立起指向我們管理的內存的指針。這三個變量都是全局變量 :
清單 1. 我們的簡單分配程序的全局變量
int has_initialized = 0;
void *managed_memory_start;
void *last_valid_address;
如前所述,被映射的內存的邊界(最後一個有效地址)常被稱爲系統中斷點或者 當前中斷點。在很多 UNIX ? 系統中,爲了指出當前系統中斷點,必須使用 sbrk(0) 函數。
sbrk 根據參數中給出的字節數移動當前系統中斷點,然後返回新的系統中斷點。使用參數 0 只是返回當前中斷點。這裏是我們的 malloc 初始化代碼,它將找到當前中斷點並初始化我們的變量:
清單 2. 分配程序初始化函數
/* Include the sbrk function */
#include
void malloc_init()
{
/* grab the last valid address from the OS */
last_valid_address = sbrk(0);
/* we don't have any memory to manage yet, so
*just set the beginning to be last_valid_address
*/
managed_memory_start = last_valid_address;
/* Okay, we're initialized and ready to go */
has_initialized = 1;
}
現在,爲了完全地管理內存,我們需要能夠追蹤要分配和回收哪些內存。在對內存塊進行了 free 調用之後,我們需要做的是諸如將它們標記爲未被使用的等事情,並且,在調用 malloc 時,我們要能夠定位未被使用的內存塊。因此, malloc 返回的每塊內存的起始處首先要有這個結構:
清單 3. 內存控制塊結構定義
struct mem_control_block {
int is_available;
int size;
};
現在,您可能會認爲當程序調用 malloc 時這會引發問題 —— 它們如何知道這個結構?答案是它們不必知道;在返回指針之前,我們會將其移動到這個結構之後,把它隱藏起來。這使得返回的指針指向沒有用於任何其他用途的 內存。那樣,從調用程序的角度來看,它們所得到的全部是空閒的、開放的內存。然後,當通過 free() 將該指針傳遞回來時,我們只需要倒退幾個內存字節就可以再次找到這個結構。
在討論分配內存之前,我們將先討論釋放,因爲它更簡單。爲了釋放內存,我們必須要做的惟一一件事情就是,獲得我們給出的指針,回退 sizeof(struct mem_control_block) 個字節,並將其標記爲可用的。這裏是對應的代碼:
清單 4. 解除分配函數
void free(void *firstbyte) {
struct mem_control_block *mcb;
/* Backup from the given pointer to find the
* mem_control_block
*/
mcb = firstbyte - sizeof(struct mem_control_block);
/* Mark the block as being available */
mcb->is_available = 1;
/* That's It! We're done. */
return;
}
如您所見,在這個分配程序中,內存的釋放使用了一個非常簡單的機制,在固定時間內完成內存釋放。分配內存稍微困難一些。以下是該算法的略述:
清單 5. 主分配程序的僞代碼
1. If our allocator has not been initialized, initialize it.
2. Add sizeof(struct mem_control_block) to the size requested.
3. start at managed_memory_start.
4. Are we at last_valid address?
5. If we are :
A. We didn't find any existing space that was large enough
-- ask the operating system for more and return that.
6. Otherwise:
A. Is the current space available (check is_available from
the mem_control_block)?
B. If it is:
i) Is it large enough (check "size" from the
mem_control_block)?
ii) If so:
a. Mark it as unavailable
b. Move past mem_control_block and return the
pointer
iii) Otherwise:
a. Move forward "size" bytes
b. Go back go step 4
C. Otherwise:
i) Move forward "size" bytes
ii) Go back to step 4
我們主要使用連接的指針遍歷內存來尋找開放的內存塊。這裏是代碼:
清單 6. 主分配程序
void *malloc(long numbytes) {
/* Holds where we are looking in memory */
void *current_location;
/* This is the same as current_location, but cast to a
* memory_control_block
*/
struct mem_control_block *current_location_mcb;
/* This is the memory location we will return. It will
* be set to 0 until we find something suitable
*/
void *memory_location;
/* Initialize if we haven't already done so */
if(! has_initialized) {
malloc_init();
}
/* The memory we search for has to include the memory
* control block, but the users of malloc don't need
* to know this, so we'll just add it in for them.
*/
numbytes = numbytes + sizeof(struct mem_control_block);
/* Set memory_location to 0 until we find a suitable
* location
*/
memory_location = 0;
/* Begin searching at the start of managed memory */
current_location = managed_memory_start;
/* Keep going until we have searched all allocated space */
while(current_location != last_valid_address)
{
/* current_location and current_location_mcb point
* to the same address. However, current_location_mcb
* is of the correct type, so we can use it as a struct.
* current_location is a void pointer so we can use it
* to calculate addresses.
*/
current_location_mcb =
(struct mem_control_block *)current_location;
if(current_location_mcb->is_available)
{
if(current_location_mcb->size >= numbytes)
{
/* Woohoo! We've found an open,
* appropriately-size location.
*/
/* It is no longer available */
current_location_mcb->is_available = 0;
/* We own it */
memory_location = current_location;
/* Leave the loop */
break;
}
}
/* If we made it here, it's because the Current memory
* block not suitable; move to the next one
*/
current_location = current_location +
current_location_mcb->size;
}
/* If we still don't have a valid location, we'll
* have to ask the operating system for more memory
*/
if(! memory_location)
{
/* Move the program break numbytes further */
sbrk(numbytes);
/* The new memory will be where the last valid
* address left off
*/
memory_location = last_valid_address;
/* We'll move the last valid address forward
* numbytes
*/
last_valid_address = last_valid_address + numbytes;
/* We need to initialize the mem_control_block */
current_location_mcb = memory_location;
current_location_mcb->is_available = 0;
current_location_mcb->size = numbytes;
}
/* Now, no matter what (well, except for error conditions),
* memory_location has the address of the memory, including
* the mem_control_block
*/
/* Move the pointer past the mem_control_block */
memory_location = memory_location + sizeof(struct mem_control_block);
/* Return the pointer */
return memory_location;
}
這就是我們的內存管理器。現在,我們只需要構建它,並在程序中使用它即可。
5. malloc()的其他實現
malloc() 的實現有很多,這些實現各有優點與缺點。在設計一個分配程序時,要面臨許多需要折衷 的選擇,其中包括:
分配的速度。
回收的速度。
有線程的環境的行爲。
內存將要被用光時的行爲。
局部緩存。
簿記(Bookkeeping)內存開銷。
虛擬內存環境中的行爲。
小的或者大的對象。
實時保證。
每一個實現都有其自身的優缺點集合。在我們的簡單的分配程序中,分配非常慢,而回收非常快。另外,由於它在使用虛擬內存系統方面較差,所以它最適於處理大的對象。
還有其他許多分配程序可以使用。其中包括:
Doug Lea Malloc:Doug Lea Malloc 實際上是完整的一組分配程序,其中包括 Doug Lea 的原始分配程序,GNU libc 分配程序和 ptmalloc。
Doug Lea 的分配程序有着與我們的版本非常類似的基本結構,但是它加入了索引,這使得搜索速度更快,並且可以將多個沒有被使用的塊組合爲一個大的塊。它還支持緩存, 以便更快地再次使用最近釋放的內存。 ptmalloc 是 Doug Lea Malloc 的一個擴展版本,支持多線程。在本文後面的 參考資料 部分中,有一篇描述
Doug Lea 的 Malloc 實現的文章。
BSD Malloc:BSD Malloc 是隨 4.2 BSD 發行的實現,包含在 FreeBSD 之中,這個分配程序可以從預先確實大小的對象構成的池中分配對象。它有一些用於對象大小的
size 類,這些對象的大小爲 2 的若干次冪減去某一常數。所以,如果您請求給定大小的一個對象,它就簡單地分配一個與之匹配的 size 類。這樣就提供了一個快速的實現,但是可能會浪費內存。在 參考資料部分中,有一篇描述該實現的文章。
Hoard:編寫 Hoard 的目標是使內存分配在多線程環境中進行得非常快。因此,它的構造以鎖的使用爲中心,從而使所有進程不必等待分配內存。它可以顯著地加快那些進行很多分配和回收的多線程進程的速度。在 參考資料部分中,有一篇描述該實現的文章。
衆多可用的分配程序中最有名的就是上述這些分配程序。如果您的程序有特別的分配需求,那麼您可能更願意編寫一個定製的能匹配您的程序內存分配方式的分配程序。不過,如果不熟悉分配程序的設計,那麼定製分配程序通常會帶來比它們解決的問題更多的問題。
附::
C++中內存的動態分配與管理永遠是一個讓C++開發者頭痛的問題,本文通過對C++中內存的動態分配釋放的基本原理的介紹,讓讀者朋友能對C++中的內存的動態分配與釋放有較爲深入的理解,從而更好駕馭C++程序。
1. 函數(Function)
(1) operator new function
1 2 |
上面是C++中operator new function的原型,一個是全局類型的,一個的類成員類型的。全局類型的operator new函數在下面兩種情況下被調用:一種是在分配C++內建(built-in)類型的動態內存時,一種是在分配用戶沒有自己定義operator new成員函數的用戶自定義類型的動態內存時。 如果用戶在自己定義的類型中,定義了operator new函數,那麼用戶在用new申請該類型的動態內存時, 便會調用該類型的成員函數operator new, 而不是全局的operator new。
另外,我們注意到,上面的原型中函數的返回值爲void *類型, 第一個參數爲size_t類型,這個是C++編譯器要求的,如果要自己重載operator new函數,返回值必須爲void* 類型,第一個參數必須爲size_t類型,否則,編譯器會返回如下錯誤信息:
1 |
error: ‘operator new’ takes type ‘size_t’ (‘unsigned int’) as first parameter |
這裏需要注意的一點是,我們可以利用operator new function可以重載的特點,可以通過參數傳入一些額外的信息,來調試程序,檢測內存泄露等。比如,我們可以像下面這樣重載,傳入調用處的行號,函數名,這樣就可以跟蹤內存的分配使用情況:
1 2 3 4 5 |
(2) operator delete function
1 2 |
上面是operator delete function的原型。operator delete function也有全局的和類成員的兩種。這裏需要注意,一個類只能有一個operator delete function做爲其成員函數,而且必須爲上面兩種中的其中一種,沒有其它的形式。如果一個類實現了自己的operator delete function成員函數,那麼在釋放該類型的內存時,編譯器便會調用成員operator delete function, 而不是全局的。
上面的兩種原型,第一種,在調用的時候,編譯器會把要釋放的內存的首地址傳入,第二種,在調用的時候,編譯器會把要釋放的內存的首地址和大小都傳 入。因此,可以利用這一特性,如果我們在基類中實現第二種形式的operator delete function的成員函數,那麼便可以用之來釋放子類類型的內存(具體參考最後面的例子)。
2. 運算符(Operator)
(1) new operator
1 2 |
注:上面的’[]‘表示在其中的部分是optional(可選的)
上面是new operator的原型。在C++中,動態內存的分配,通常都是調用new operator來完成的,利用new operator來分配動態內存,編譯器要做下面兩項工作:
- a. 調用operator new function分配內存(allocate the memory)
- b. 調用構造函數(call the constructor)來進行初始化
下面來說一說new operator的原型中各部分到底是幹什麼的:
placement: 如果你重載了operator new function, placement可以用來傳遞額外的參數
type-name: 指定要分配的內存的類型,可以是內建(built-in)類型,也可以是用戶自定義類型
new-initializer: 指定對分配後內存的初始化的參數,也就的構造函數的參數 。這裏需要注意一點,在分配一個對象的數組類型的內存時,不能夠指定初始化參數;換言之,要想分配一個對象的數組類型的內存,該對象必須有缺省構造函數
(2) delete opeartor
1 2 |
上面是delete operator的原型,第一種用來釋放普通的對象(包括內建類型)類型的內存,第二種用來釋放對象的數組類型的內存。在C++中,用new operator分配的動態內存,必須調用delete operator來釋放,通常用delete operator釋放內存編譯器要做下面兩項工作:
- a. 調用對象析構函數來析構對象
- b. 調用operator delete function來釋放內存(deallocate the memory)
3. 關於new/delete使用過程中一些需要注意的點
(1)如何區別operator new/delete function 與 new/delete operator ?
通過上面的講述,不難看出,我們分配/釋放動態內存,調用的是new/delete operator, 而在調用new/delete的過程中,編譯器會自動調用operator new/delete function來完成實際的內存分配/釋放的工作
(2) 用delete operator去釋放一塊不是由new operator釋放的內存,結果是不可預料的,因此,切記,operator new與operator delete一定要配對使用,這是寫好程序的基礎
(3) new operator調用失敗會拋出std::bad_alloc異常,前提是你沒有自己重載對應的operator new function;delete operator失敗,常見的原因是多次delete同一塊內存
(4) 如果一塊內存被delete後,再對它解引用(Dereference),結果也是不可預測的,很可能導致程序崩潰
(5) delete一個空(NULL)指針是安全的,沒有任何害處的
(6) 類成員類型的operator new/delete函數必須爲靜態(static)函數,因此它們不能爲虛函數(virtual function),也遵守public, protected, private的訪問權限控制
4. 關於上面所講的內容的一些例子:
程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
運行結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
wuzesheng@wuzesheng-ubuntu:~/Program$ ./a.out operator new called constructor called ####### operator new called, line: 68, func: main constructor called ####### operator new called constructor called constructor called constructor called constructor called constructor called constructor called constructor called constructor called constructor called constructor called ####### ~A() delete2: 8 ####### ~B() ~A() delete2: 16 ####### ~A() ~A() ~A() ~A() ~A() ~A() ~A() ~A() ~A() ~A() delete1 ####### delete1 |
上面的程序很簡單,我在這裏不做過多的解釋,感興趣的朋友可以自己分析一下。
通過我上面的講解,相信大多數朋友應該對C++中內存的動態分配與釋放有了較爲深入的理解。後續我還有可能寫一些關於C++中內存管理的文章,只有把本文所講的內容與後續的內存管理的一些常見的方法結合起來,我們才寫出更加健壯的C++程序。歡迎讀者朋友留言一起交流!