1. 程式人生 > >C語言的本質(25)——C標準庫之記憶體管理

C語言的本質(25)——C標準庫之記憶體管理

程式中需要動態分配一塊記憶體時怎麼辦呢?我們可以定義一個緩衝區陣列,但是這種方法不夠靈活,C89要求定義的陣列是固定長度的,而程式往往在執行時才知道要動態分配多大的記憶體,例如:

void foo(char *str, int n)
{
         charbuf[?];
         strncpy(buf,str, n);
......
}

n是由引數傳進來的,事先不知道是多少,那麼buf該定義多大呢?在第 1 節 “陣列的基本操作”講過C99引入VLA特性,可以定義charbuf[n+1] = {};,這樣可確保buf是以'\0'結尾的。但即使用VLA仍然不夠靈活,VLA是在棧上動態分配的,函式返回時就要釋放,如果我們希望動態分配一塊全域性的記憶體空間,在各函式中都可以訪問呢?由於全域性陣列無法定義成VLA,所以仍然不能滿足要求。

 程序有一個堆空間,C標準庫函式malloc可以在堆空間動態分配記憶體,它的底層通過brk系統呼叫向作業系統申請記憶體。動態分配的記憶體用完之後可以用free釋放,更準確地說是歸還給malloc,這樣下次呼叫malloc時這塊記憶體可以再次被分配。

 下面詳細說明這兩個函式的用法和工作原理。

#include <stdlib.h>
void *malloc(size_t size);

返回值:成功返回所分配記憶體空間的首地址,出錯返回NULL

void free(void *ptr);

malloc的引數size表示要分配的位元組數,如果分配失敗(可能是由於系統記憶體耗盡)則返回NULL。由於malloc函式不知道使用者拿到這塊記憶體要存放什麼型別的資料,所以返回通用指標void *,使用者程式可以轉換成其它型別的指標再訪問這塊記憶體。malloc函式保證它返回的指標所指向的地址滿足系統的對齊要求,例如在32位平臺上返回的指標一定對齊到4位元組邊界,以保證使用者程式把它轉換成任何型別的指標都能用。

動態分配的記憶體用完之後可以用free釋放掉,傳給free的引數正是先前malloc返回的記憶體塊首地址。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
typedef struct {
         intnumber;
         char*msg;
} unit_t;
 
int main(void)
{
         unit_t*p = malloc(sizeof(unit_t));
 
         if(p == NULL) {
                   printf("outof memory\n");
                   exit(1);
         }
         p->number= 3;
         p->msg= malloc(20);
         strcpy(p->msg,"Hello world!");
         printf("number:%d\nmsg: %s\n", p->number, p->msg);
         free(p->msg);
         free(p);
         p= NULL;
 
         return0;
}

關於這個程式需要注意:

 unit_t *p = malloc(sizeof(unit_t));這一句,等號右邊是void*型別,等號左邊是unit_t *型別,編譯器會做隱式型別轉換,我們講過void *型別和任何指標型別之間可以相互隱式轉換。

 雖然記憶體耗盡是很不常見的錯誤,但寫程式要規範,malloc之後應該判斷是否成功。以後要學習的大部分系統函式都有成功的返回值和失敗的返回值,每次呼叫系統函式都應該判斷是否成功。

 free(p);之後,p所指的記憶體空間是歸還了,但是p的值並沒有變,因為從free的函式介面來看根本就沒法改變p的值,p現在指向的記憶體空間已經不屬於使用者,換句話說,p成了野指標,為避免出現野指標,我們應該在free(p);之後手動置p = NULL;。

 應該先free(p->msg),再free(p)。如果先free(p),p成了野指標,就不能再通過p->msg訪問記憶體了。

上面的例子只有一個簡單的順序控制流程,分配記憶體,賦值,列印,釋放記憶體,退出程式。這種情況下即使不用free釋放記憶體也可以,因為程式退出時整個程序地址空間都會釋放,包括堆空間,該程序佔用的所有記憶體都會歸還給作業系統。但如果一個程式長年累月執行(例如網路伺服器程式),並且在迴圈或遞迴中呼叫malloc分配記憶體,則必須有free與之配對,分配一次就要釋放一次,否則每次迴圈都分配記憶體,分配完了又不釋放,就會慢慢耗盡系統記憶體,這種錯誤稱為記憶體洩漏(Memory Leak)。另外,malloc返回的指標一定要儲存好,只有把它傳給free才能釋放這塊記憶體,如果這個指標丟失了,就沒有辦法free這塊記憶體了,也會造成記憶體洩漏。例如:

void foo(void)
{
         char*p = malloc(10);
......
}

foo函式返回時要釋放區域性變數p的記憶體空間,它所指向的記憶體地址就丟失了,這10個位元組也就沒法釋放了。記憶體洩漏的Bug很難找到,因為它不會像訪問越界一樣導致程式執行錯誤,少量記憶體洩漏並不影響程式的正確執行,大量的記憶體洩漏會使系統記憶體緊缺,導致頻繁換頁,不僅影響當前程序,而且把整個系統都拖得很慢。

關於malloc和free還有一些特殊情況。malloc(0)這種呼叫也是合法的,也會返回一個非NULL的指標,這個指標也可以傳給free釋放,但是不能通過這個指標訪問記憶體。free(NULL)也是合法的,不做任何事情,但是free一個野指標是不合法的,例如先呼叫malloc返回一個指標p,然後連著呼叫兩次free(p);,則後一次呼叫會產生執行時錯誤。

下面的圖簡單的表示malloc和free的工作原理。真正的實現比這要複雜得多,但基本工作原理也是如此。

圖中白色背景的框表示malloc管理的空閒記憶體塊,深色背景的框不歸malloc管,可能是已經分配給使用者的記憶體塊,也可能不屬於當前程序,Break之上的地址不屬於當前程序,需要通過brk系統呼叫向核心申請。每個記憶體塊開頭都有一個頭節點,裡面有一個指標欄位和一個長度欄位,指標欄位把所有空閒塊的頭節點串在一起,組成一個環形連結串列,長度欄位記錄著頭節點和後面的記憶體塊加起來一共有多長,以8位元組為單位(也就是以頭節點的長度為單位)。

一開始堆空間由一個空閒塊組成,長度為7×8=56位元組,除頭節點之外的長度為48位元組。

呼叫malloc分配8個位元組,要在這個空閒塊的末尾截出16個位元組,其中新的頭節點佔了8個位元組,另外8個位元組返回給使用者使用,注意返回的指標p1指向頭節點後面的記憶體塊。

又呼叫malloc分配16個位元組,又在空閒塊的末尾截出24個位元組,步驟和上一步類似。

呼叫free釋放p1所指向的記憶體塊,記憶體塊(包括頭節點在內)歸還給了malloc,現在malloc管理著兩塊不連續的記憶體,用環形連結串列串起來。注意這時p1成了野指標,指向不屬於使用者的記憶體,p1所指向的記憶體地址在Break之下,是屬於當前程序的,所以訪問p1時不會出現段錯誤,但在訪問p1時這段記憶體可能已經被malloc再次分配出去了,可能會讀到意外改寫資料。另外注意,此時如果通過p2向右寫越界,有可能覆蓋右邊的頭節點,從而破壞malloc管理的環形連結串列,malloc就無法從一個空閒塊的指標欄位找到下一個空閒塊了。

呼叫malloc分配16個位元組,現在雖然有兩個空閒塊,各有8個位元組可分配,但是這兩塊不連續,malloc只好通過brk系統呼叫擡高Break,獲得新的記憶體空間。在[K&R]的實現中,每次呼叫sbrk函式時申請1024×8=8192個位元組,在Linux系統上sbrk函式也是通過brk實現的,這裡為了畫圖方便,我們假設每次呼叫sbrk申請32個位元組,建立一個新的空閒塊。

新申請的空閒塊和前一個空閒塊連續,因此可以合併成一個。在能合併時要儘量合併,以免空閒塊越割越小,無法滿足大的分配請求。

在合併後的這個空閒塊末尾截出24個位元組,新的頭節點佔8個位元組,另外16個位元組返回給使用者。

呼叫free釋放這個記憶體塊,由於它和前一個空閒塊連續,又重新合併成一個空閒塊。注意,Break只能擡高而不能降低,從核心申請到的記憶體以後都歸malloc管理了,即使呼叫free也不會還給核心。

除了malloc之外,C標準庫還提供了另外兩個在堆空間分配記憶體的函式,它們分配的記憶體同樣由free釋放。

#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);

返回值:成功返回所分配記憶體空間的首地址,出錯返回NULLcalloc的引數很像fread/fwrite的引數,分配nmemb個元素的記憶體空間,每個元素佔size位元組,並且calloc負責把這塊記憶體空間用位元組0填充,而malloc並不負責把分配的記憶體空間清零。

有時候用malloc或calloc分配的記憶體空間使用了一段時間之後需要改變它的大小,一種辦法是呼叫malloc分配一塊新的記憶體空間,把原記憶體空間中的資料拷到新的記憶體空間,然後呼叫free釋放原記憶體空間。使用realloc函式簡化了這些步驟,把原記憶體空間的指標ptr傳給realloc,通過引數size指定新的大小(位元組數),realloc返回新記憶體空間的首地址,並釋放原記憶體空間。新記憶體空間中的資料儘量和原來保持一致,如果size比原來小,則前size個位元組不變,後面的資料被截斷,如果size比原來大,則原來的資料全部保留,後面長出來的一塊記憶體空間未初始化(realloc不負責清零)。注意,引數ptr要麼是NULL,要麼必須是先前呼叫malloc、calloc或realloc返回的指標,不能把任意指標傳給realloc要求重新分配記憶體空間。作為兩個特例,如果呼叫realloc(NULL, size),則相當於呼叫malloc(size),如果呼叫realloc(ptr, 0),ptr不是NULL,則相當於呼叫free(ptr)。

#include <alloca.h>
void *alloca(size_t size);

返回值:返回所分配記憶體空間的首地址,如果size太大導致棧空間耗盡,結果是未定義的引數size是請求分配的位元組數,alloca函式不是在堆上分配空間,而是在呼叫者函式的棧幀上分配空間,類似於C99的變長陣列,當呼叫者函式返回時自動釋放棧幀,所以不需要free。這個函式不屬於C標準庫,而是在POSIX標準中定義的。