1. 程式人生 > >C程式記憶體管理

C程式記憶體管理

C程式的記憶體管理

熟悉Java語言的肯定知道,Java中記憶體管理是由虛擬機器幫助我們完成的,在C/C++中可不是這樣,程式設計師需要自己去分配和回收記憶體空間。本文記錄了C程式可執行檔案的儲存結構、在記憶體中的儲存結構等方面的內容。以下C程式所使用的編譯器版本是GCC 4.4.7。

從一個C程式說起

檔案的結構

對於以下這段Hello.c程式再熟悉不過了

#include<stdio.h>

int main(void)
{
        printf("Hello World\n");
        return 0;
}

下面使用gcc編譯它,然後執行可執行檔案,再檢視可執行檔案的儲存結構


可以看出,可執行檔案Hello在儲存時(沒有調入記憶體時)分為程式碼區(text),資料區(data)和未初始化資料區(bss)3個部分。另外3個欄位中,dec表示十進位制總和,hex表示十六進位制總和,filename表示檔名。各段的具體說明如下:

(1)程式碼段(text segment):存放CPU執行的機器指令。通常程式碼區是可以共享的(即另外的執行程式可以呼叫它)。程式碼區通常是隻讀的,以防止程式意外的修改它的指令。常量資料在編譯時在程式碼區分配記憶體。程式碼區的指令包括操作碼和操作物件(或物件的地址引用)。如果是立即數,就直接包含在程式碼中;如果是區域性資料,將在執行時的棧空間中分配,然後在引用該資料的地址;如果是bss區和資料區,在程式碼中同樣是引用該資料的地址。

(2)全域性初始化資料區/靜態資料區(initialized data segment/data segment),或者簡稱資料段:該區域包含了在程式中明確被初始化的全域性變數,已經初始化的靜態變數(包括全域性靜態變數和區域性靜態變數)。需要注意的是,被const宣告的變數和字串常量在程式碼段中分配記憶體。這和組合語言中的資料段的概念是類似的。

(3)未初始化資料區bss(Block Started By Symbol):儲存的是未初始化的全域性變數和未初始化的靜態變數。bss區域的資料在程式執行前會被核心初始化為0或者空指標(NULL),這和棧中的變數是不同的,棧中的變數(區域性變數)如果沒有初始化就使用,系統會隨機分配一個值給它,這是不安全的。

上述這些都是可執行檔案的儲存結構分析,其實執行時的記憶體結構和這個十分類似,只不過多了堆記憶體和棧記憶體區域,在後面會分析到。下面通過幾個例子驗證之。

還是以Hello.c程式為例


我們在Hello.c中增加了一句程式碼,定義一個常量i,通過分析比較,可以發現程式碼段text區大小增加了4個位元組(一個int型別佔4個位元組),其他區域不變,可知常量是分配在程式碼段的。

在上述的基礎上,在新增一句,定義一個全域性變數a,並給它賦值為2,觀察各區域變化

通過比較發現,只有資料段的大小增加了4個位元組,也證明了明確被初始化的全域性變數是被分配在資料區的。靜態變數也是一樣,可自行證之。

在上述的基礎上,我們在定義一個全域性變數b,但是這一個不要賦值,觀察各區域變化


可以發現,這一次只有bss區域增加4個位元組,也證明了未初始化的全域性變數是分配在bss區域的。未初始化的靜態變數同理,可自行證之。

程序的結構

個程式執行的時候就表現為一個或者多個程序,其實程序核心的資料結構和上述檔案的儲存結構很相似,主要是多了堆記憶體和棧記憶體區域。主要的佈局如下圖所示


各部分說明如下:

(1)程式碼區(text segment):載入的是上述可執行檔案的程式碼段,其載入到記憶體中的位置由載入器完成。

(2)全域性初始化資料區/靜態資料區(Data Segment):載入的是上述可執行檔案的資料段,位置位於可執行程式碼段後面,可以是不相連的。在程式執行之初就為資料段申請了空間,程式退出的時候釋放空間,其生命週期是整個程式的執行時期。

(3)未初始化資料區(BSS):載入的是上述可執行檔案的BSS段,位置在資料段之後,可以不相連。其生命週期和資料段一樣。

(4)棧區(Stack):由編譯器自動分配釋放,存放函式的引數值、返回值、區域性變數等。在程式執行過程中動態的分配和釋放,棧區位於BSS後,是向上有限擴充套件的。

(5)堆區(Heap):用於動態記憶體分配。位於棧區的後面,是向下有限擴充套件的。一般由程式設計師進行分配和釋放,若不釋放,在程式結束的時候,由OS負責回收。

堆與棧的區別

棧是由編譯器在程式執行時分配的記憶體空間,由作業系統維護(這和Java虛擬機器中的棧記憶體是類似的)。堆是由malloc( )函式(C++中的new)分配記憶體,記憶體的管理由程式設計師手動控制,在C語言中使用free( )函式完成釋放(C++中是delete)。堆和棧的主要區別有以下幾點:

(1)管理方式不同。程式在執行時,棧由作業系統自動管理,堆由程式設計師手動管理,堆記憶體的管理更容易造成記憶體的洩漏。

(2)空間大小不同。棧是向低地址擴充套件的(參考上圖)是一塊連續的記憶體空間,棧的容量是預先設定好的,如果申請的棧空間大於該預設值,將會出現棧溢位錯誤。堆是向高地址擴充套件的,是不連續的記憶體空間。系統是採用連結串列管理空閒的記憶體地址的,且連結串列的遍歷方向是由低地址向高地址的。

(3)產生的碎片不同。在堆中頻繁的使用malloc/free(new/delete)勢必會再次記憶體空間的不連續,產生大量的記憶體碎片,使程式執行效率降低。而在棧記憶體中,則完全不會存在這樣的問題。

(4)擴充套件方向不同。在x86平臺上,堆是向上擴充套件的,即向記憶體地址增加的方向;棧是向下擴充套件的,即向記憶體地址減小的方向。

和前面一樣,下面使用一個例子去驗證執行時的儲存分佈

#include <stdio.h>
#include <malloc.h>
#include <unistd.h>
#include <alloca.h>

extern void afunc(void);
extern etext,edata,end;<span style="white-space:pre">			</span>

int bss_var;                            //no init globel data must be in bss

int data_var=42;                        //init globel data must be in data

#define SHW_ADR(ID,I) printf("the %8s\t is at adr:%8x\n",ID,&I);                //the macro to printf the addr

int main(int argc,char *argv[])
{
        char *p,*b,*nb;

        printf("Adr etext:%8x\t Adr edata %8x\t Adr end %8x\t\n",&etext,&edata,&end);

        printf("\ntext Location:\n");
        SHW_ADR("main",main);                   //text section function
        SHW_ADR("afunc",afunc);                 //text section function

        printf("\nbss Location:\n");
        SHW_ADR("bss_var",bss_var);             //bss section var

        printf("\ndata location:\n");
        SHW_ADR("data_var",data_var);   //data section var


        printf("\nStack Locations:\n");
        afunc();

        p=(char *)alloca(32);                   //alloc memory from statck
        if(p!=NULL)
        {
                SHW_ADR("start",p);
                SHW_ADR("end",p+31);
        }

        b=(char *)malloc(32*sizeof(char));      //malloc memory from heap
        nb=(char *)malloc(16*sizeof(char));

        printf("\nHeap Locations:\n");
        printf("the Heap start: %p\n",b);
        printf("the Heap end:%p\n",(nb+16*sizeof(char)));
        printf("\nb and nb in Stack\n");
        SHW_ADR("b",b);
        SHW_ADR("nb",nb);
        free(b);
        free(nb);
}


void afunc(void)
{
        static int long level=0;        //data section static var
        int      stack_var;                             //temp var ,in stack section
        if(++level==5)
        {
                return;
        }
        SHW_ADR("stack_var in stack section",stack_var);
        SHW_ADR("Level in data section",level);
        afunc();
}

其中需要說明的,etext、edata和end(可以理解為end of text、end of data和end of bss)是3個外部的全域性變數,是跟使用者程序有關的虛擬地址,分別標誌著程式碼段的結束、資料段的結束和bss段的結束。

使用gcc編譯上述程式,並執行。


通過觀察執行時各區域的記憶體地址,可以得到以下的記憶體分佈圖(每一個執行環境,下面的記憶體地址值會不一樣,但對應關係不變)


需要說明的是,各區域之間並不是相連的,各區域在圖中的大小,並不代表實際的大小。從上面還可以發現,afunc函式中的靜態變數level四次列印的地址是一樣的,而區域性變數stack_var四次列印的地址各不相同,也就是說靜態變數在整個程式的生命週期內只會被載入初始化一次,而且分配在資料段;而區域性變數被分配到棧區,其生命週期是當前函式,每一次進入函式都會重新載入初始stack_var。