C語言之內存管理

原文出處:http://blog.csdn.net/jiaxiongxu/article/details/6613315

本文主要是以菜鳥的角度看C語言內存管理,分析malloc最基本的實現方法,如果已經知道malloc的實現方法的大鳥們,可以直接忽略本文了,呵呵。

在8086彙編時代裏,是沒有全局變量和局部變量之分的,通常的做法是:1、自己選定一片內存空間,用僞指令起個別名就當作全局變量來用。2、自己選一片內存空間作爲棧,用push和pop操作棧就是操作局部變量了。

這樣做法有很明顯的問題——所有變量甚至棧的內存地址都是自己任意選定的,假如數據量龐大的時候,很容易搞不清哪些地址已經使用,哪些沒有使用。假如自己定義的變量有地址重複或者和系統重要的變量地址重複,那麼後果就不堪設想了(比野指針破壞力更大!)。

下面來談談C語言裏面的內存管理。在C語言裏,我們完全可以不用自己選定內存,試回想一下我們C語言編程時有幾種定義變量的方式:1、在函數裏定義臨時變量。2、在函數外定義全局變量。3、定義指針指向用malloc申請的空間裏等。是完全不用自己去選定內存,不用自己去考慮哪些內存地址使用沒使用的吧~!(沒用過彙編的同學們可能沒有內存管理的概念,以爲內存分配全都是系統做的事,其實很大一部分是語言設計者做的工作,甚至我們也可以自己來管理,這是C語言強大的體現^ ^)

接着解讀:

1、函數裏定義的臨時變量是自動分配到棧裏,隨着函數的返回而自動釋放(釋放的意思是:告訴進程這個內存空間不再使用了,可以分配給其他變量)。

2、函數外定義的全局變量是在靜態存儲區域裏分配的,靜態存儲區域就是編譯時已經劃分好的區域(反正就是專門放全局變量和靜態變量的地方^ ^)在進程整個生命週期裏一直都在,直到進程結束才被釋放。

3、malloc申請的空間在堆裏(堆和棧都是一種數據結構,不懂也沒關係,知道它是專門用來放malloc這類動態分配的內存就行了^ ^),malloc這類動態分配是C語言內存管理最美妙的地方,他會自動在堆裏尋找合適你想申請大小的內存區並返回,而且他知道哪些內存用過哪些沒用過,絕對不會出現變量內存重疊的情況出現(除非你越界了-。-),而且當內存碎片很多、連續空間不足的時候malloc會移動內存將零碎的內存片組合成大的,厲害吧!malloc申請的空間一直都存在直到進程結束或者用free釋放。最後,切忌,爲了體現C語言高效的內存管理特性,使用malloc完之後必須要用free來釋放申請的內存。


malloc如此美妙,那麼他是怎樣實現的呢?

我們定義指針p指向用malloc申請的內存,就可以想平時操作變量那樣自由地在這片內存空間上存取數據,釋放時,調用free(p)就可以釋放這片內存空間了。仔細一想,這裏拋出了兩個問題:1、free的參數p只是一個指針,如何知道p申請的內存空間大小呢? 2、根據前面所說釋放的定義,free如何通知進程p這個內存空間不用了,以便下次malloc可以重複使用呢?


根據我們編程的經驗,肯定在某個地方有專門的兩個字段用於存儲對應內存段的大小和是否被利用的標記位。(malloc在不同編程環境中,有不同的實現,但是主要思想還是一樣的,本文講的就是最基本最簡單的一種實現方式。)

如上所說標誌信息通常爲:

  1. struct mem_block_tag {  
  2.     int is_available;                   //標記此空間是否正在被利用   
  3.     size_t size;                        //此空間的大小   
  4. };  


那麼標誌信息應該放在什麼地方纔可以保證不影響malloc返回指針的正常使用呢?通常的做法是放在p指針指向的地址之前——好處是在開發者不越界的情況下永遠不會修改到標誌消息的。如圖下圖所示:

  1. void* p1 = malloc(cSize1);  
  2. void* p2 = malloc(cSize2);  


malloc返回開發者所需要的內存空間地址,並在此地址前加上標記頭。如此每次調用malloc時,malloc就能根據標誌頭的is_available知道這片內存能不能用,根據size就能知道這片區域是否足夠大並且同時定位到下一片內存空間(本標記頭地址+本內存大小==下個內存標記頭地址 羅)。

我們大概瞭解了malloc是怎麼找到未使用合適大小的內存區(根據每段內存空間的標記頭)。那麼free函數的實現就自然明瞭了:

  1. void free(void *ptr)  
  2. {  
  3.     struct mem_block_tag *pTag;  
  4.     pTag = ptr - sizeof(struct mem_block_tag); //找到標記頭   
  5.     pTag->is_available = 1;                    //將空間標記爲未使用,就等於告訴malloc這個內存空間不再使用了,可以分配給其他變量咯   
  6. }  

最後malloc得先擁有一片內存區域才能在其上進行內存管理。之前我們說過malloc申請的內存是在堆上的,怎麼找到未用的堆內存呢?

linux裏提供標準C函數sbrk來增加程序可用數據段內存空間,返回值是該新內存空間的首地址。

注意:某個堆內存要用sbrk函數增加後才能使用,不然是不能訪問的。(sbrk的作用實際上就是將虛擬內存映射到內存裏,想更深入的瞭解自行google ^ ^)

比較一下下面兩段代碼:

  1. #include <stdio.h>   
  2. #include <malloc.h>   
  3. int main()  
  4. {  
  5.     int* p1=(int*)malloc(0);  
  6.     printf("%d\n",*p1);        //這裏先不管*p1是什麼值,只要p1內存可讀,就肯定會輸出一個值   
  7.     printf("Hello World");  
  8.     return 0;  
  9. }  
  1. 輸出結果:  
  2. 0                     
  3. Hello World  

  1. #include <stdio.h>   
  2. #include <unistd.h>   
  3. int main()  
  4. {  
  5.     int* p1=(int*)sbrk(0);  
  6.     printf("%d\n",*p1);       //這裏先不管*p1是什麼值,只要p1內存可讀,就肯定會輸出一個值    
  7.     printf("Hello World");  
  8.     return 0;  
  9. }  
  1. 沒有輸出結果,程序運行到printf("%d\n",*p1);中斷退出,證明p1地址的內存不可讀。  

因爲sbrk返回是新增內存的首地址,而這裏用sbrk(0)增加0大小的內存,即返回的地址是沒用sbrk增加的,由此可證明,對於進程來講,沒用sbrk增加的堆內存區域是不可讀的。

而且用sbrk增加的內存空間都是在原來空間上往後加,是連續的一片的。

所以malloc實現的策略是,在最開始的時候用sbrk先取得一片內存空間用來進行內存管理。之後每次調用malloc先檢查已有的堆內存有沒有足夠大可用的,若沒有則再用sbrk去增加。

從這裏可知道,必須還有三個全局變量分別標記:是否初始化(是否已用sbrk取得最處內存空間)、已有堆內存的首地址,已有堆內存的尾地址。

  1. int has_initialized = 0;  
  2. void *managed_memory_start;  
  3. void *managed_memory_end;  

在windows裏並沒有提供sbrk函數(sbrk是標準的C庫函數呢-。-)

通常的做法是先用系統提供的malloc申請一片大空間,再在上面進行內存管理實現自己malloc函數。

有同學會說:既然已經提供了malloc爲什麼還要自己實現呢?...

第一,學習瞭解C語言的內存管理機制。

第二,系統提供的malloc是萬能的什麼情況都能用,但對於具體某個程序不一定是最優的,某些情況下可能就需要針對自己程序量身定做一套內存管理機制了。


更具體實現的細節這篇文章《malloc的實現原理學習》已經非常詳細了,這裏就不重複了。


大家快點去實現吧。^ ^

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章