C語言的之所以復雜,首先它的內存模型功不可沒。不像某些那樣的高級語言只需要在使用對象的時候,用new創建。所有之後的事情,你不需要操心。對於C語言,所有與內存相關的東西,都需要熟悉,否則,時間一久,總會踩著雷。下圖是典型的一個C程序的內存結構,當然還有一個重要的前提,這樣的一種布局是在虛擬內存中的:
1 int main() 2 { 3 int* p = sbrk(100); 4 *(p+1023) =4; 5 printf("**\n"); 6 *(p+1024) =4; 7 }
這樣一段代碼,向內核申請100字節的內存,實際上映射的是一個內存頁,行4訪問內存頁的最後4個字節並且改寫,行6訪問映射關系之外的內存顯然是非法的,程序的運行結果如下:
$a.out
**
Segmentation fault
用brk()/sbrk()釋放內存時,也不定會立即解除映射關系。當program break 下降超過一個頁時,才有可能將申請的物理內存返還給內核。當然釋放之後所有的對這塊內存的操作都是未定義的,與玩火無異。同時program break移動還要注意的一點就是,program break的位置不能移動到heap區之外的地方,比如bss區,數據區等等,這樣的行為基本也屬於作死的行為之中。
使用C標准庫函數malloc()/free() 絕對是C語言中使用最廣泛的函數之一了。相比brk()/sbrk()他接口更加簡單,也允許隨意釋放內存。(brk()/sbrk() 不能隨意釋放是由於program break往下移動的釋放內存的時候,會把頂部“無辜”的元素也釋放了。)例如這樣的情況(這裡內存映射解除了):
而free()釋放並沒有這樣的“坑”,因為free釋放內存不一定會移動program break。如果要free() 釋放的內存上方(高內存地址處)仍然有沒有釋放的內存,那麼program break就不會移動,因此也不會解除映射關系,也就是說這塊內存並沒有返還給內核。而是作為空閒的內存交給free維護去了,待下次malloc申請時,再返回這塊內存(如果夠用的話)給malloc返回。那麼free又如何知道釋放內存的大小的呢?這是由於malloc返回的內存擁有一個比較特殊的結構:
在這塊內存的前面記錄著這塊內存的大小。當回收這塊內存時,就會記錄下他的長度和地址。當再次malloc時就會比較空閒內存列表是否有符合要求的內存,交給程序“二次使用”(或者N次使用)。當然至於用不用空閒內存列表的內存還要取決於具體情況:
1.如果空余的內存比malloc申請的大,那麼就切割一部分給malloc返回,剩余的部分再看做是一塊空閒的內存,留給下次的malloc使用。
2.如果malloc時沒有合適的空閒的內存,那麼就會像普通情況那樣移動program break,或許申請新的內存(可能上回映射的時候會有富余,就不需要重新映射)。
知道了這些基本的實現之後,我們卻發現malloc()、free()是比較危險的函數了,使用申請的內存時一定要小心,特別是邊界的情況,否則結果可能是災難性的。比如這樣的一種情況,使用分配的內存後,僅僅越界了1個字節,而這一個字節恰恰記錄著另一塊內存的長度,當釋放這塊內存的時候,free維護了錯誤的長度,而下回有申請內存時把這塊內存交給malloc那麼一場“災難”便到來了。
其余的內存分配函數void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
calloc()與malloc類似,分配nmemb個大小為size的對象,但是與malloc不同的是:calloc會把分配的內存初始化為0.
realloc() 正如名字那樣是“重新分配”的意思,用來調整已經分配內存ptr的大小,如果ptr之後的內存不夠就會申請一塊新的區域,將原有內存原樣復制過去,新增加的內存不作初始化。因此返回的結果可能與ptr不同,實際上不部分時候都是不相同的。因此realloc效率是不夠高的。萬不得已的時候,建議不要使用。
void *alloca(size_t size);
作用是在棧上分配內存。manual上是這樣描述的:
The alloca() function allocates size bytes of space in the stack frame of the caller. This temporary space is automatically freed when the function that called alloca() returns to its caller.
在棧上分配內存的需要的場景不多,比如setjmp,longjmp執行非局部跳轉的時候需要使用分配的內存時,就應該考慮alloca,因為他申請的內存會自動的釋放,所以不會出現longjmp“回跳”時候,內存洩露的情況。這樣的函數偶爾用一用還是有利於身心健康的。