天天看點

堆棧、棧幀、函數調用、記憶體配置設定總結

http://blog.21ic.com/user1/5585/archives/2009/56681.html

1) 在棧上建立。在執行函數時,函數内局部變量的存儲單元都在棧上建立,函數執行結束時這些存儲單元自動被釋放。棧記憶體配置設定運算内置于處理器的指令集中,一般使用寄存器來存取,效率很高,但是配置設定的記憶體容量有限。

2) 從堆上配置設定,亦稱動态記憶體配置設定。程式在運作的時候用malloc或new申請任意多少的記憶體,程式員自己負責在何時用free或delete來釋放記憶體。動态記憶體的生存期由程式員自己決定,使用非常靈活。 

3) 從靜态存儲區域配置設定。記憶體在程式編譯的時候就已經配置設定好,這塊記憶體在程式的整個運作期間都存在。例如全局變量,static變量。 

4) 文字常量配置設定在文字常量區,程式結束後由系統釋放。 

5)程式代碼區。

經典執行個體:(代碼來自網絡高手,沒有找到原作者)

一 Code  

#i nclude <string> 

int a=0;    //全局初始化區 

char *p1;   //全局未初始化區 

 void main() 

    int b;//棧 

    char s[]="abc";   //棧 

    char *p2;         //棧 

    char *p3="123456";   //123456\0在常量區,p3在棧上。 

    static int c=0;   //全局(靜态)初始化區 

    p1 = (char*)malloc(10); 

    p2 = (char*)malloc(20);   //配置設定得來得10和20位元組的區域就在堆區。 

    strcpy(p1,"123456");   //123456\0放在常量區,編譯器可能會将它與p3所向"123456\0"優化成一個地方。 

}

二 三種記憶體對象的比較 

  棧對象的優勢是在适當的時候自動生成,又在适當的時候自動銷毀,不需要程式員操心;而且棧對象的建立速度一般較堆對象快,因為配置設定堆對象時,會調用operator new操作,operator new會采用某種記憶體空間搜尋算法,而該搜尋過程可能是很費時間的,産生棧對象則沒有這麼麻煩,它僅僅需要移動棧頂指針就可以了。但是要注意的是,通常棧空間容量比較小,一般是1MB~2MB,是以體積比較大的對象不适合在棧中配置設定。特别要注意遞歸函數中最好不要使用棧對象,因為随着遞歸調用深度的增加,所需的棧空間也會線性增加,當所需棧空間不夠時,便會導緻棧溢出,這樣就會産生運作時錯誤。 

  堆對象建立和銷毀都要由程式員負責,是以,如果處理不好,就會發生記憶體問題。如果配置設定了堆對象,卻忘記了釋放,就會産生記憶體洩漏;而如 果已釋放了對象,卻沒有将相應的指針置為NULL,該指針就是所謂的“懸挂指針”,再度使用此指針時,就會出現非法通路,嚴重時就導緻程式崩潰。但是高效的使用堆對象也可以大大的提高代碼品質。比如,我們需要建立一個大對象,且需要被多個函數所通路,那麼這個時候建立一個堆對象無疑是良好的選擇,因為我們通過在各個函數之間傳遞這個堆對象的指針,便可以實作對該對象的共享,相比整個對象的傳遞,大大的降低了對象的拷貝時間。另外,相比于棧空間,堆的容量要大得多。實際上,當實體記憶體不夠時,如果這時還需要生成新的堆對象,通常不會産生運作時錯誤,而是系統會使用虛拟記憶體來擴充實際的實體記憶體。

  靜态存儲區。所有的靜态對象、全局對象都于靜态存儲區配置設定。關于全局對象,是在main()函數執行前就配置設定好了的。其實,在main()函數中的顯示代 碼執行之前,會調用一個由編譯器生成的_main()函數,而_main()函數會進行所有全局對象的的構造及初始化工作。而在main()函數結束之 前,會調用由編譯器生成的exit函數,來釋放所有的全局對象。比如下面的代碼:

void main(void) 

… …// 顯式代碼 

}

實際上,被轉化成這樣:

void main(void) 

_main(); //隐式代碼,由編譯器産生,用以構造所有全局對象 

… … // 顯式代碼 

… … 

exit() ; // 隐式代碼,由編譯器産生,用以釋放所有全局對象 

}

  除了全局靜态對象,還有局部靜态對象通和class的靜态成員,局部靜态對象是在函數中定義的,就像棧對象一樣,隻不過,其前面多了個static關鍵字。局部靜态對象的生命期是從其所在函數第一次被調用,更确切地說,是當第一次執行到該靜态對象的聲明代碼時,産生該靜态局部對象,直到整個程式結束時,才銷毀該對象。class的靜态成員的生命周期是該class的第一次調用到程式的結束。

三 函數調用與堆棧

1)編譯器一般使用棧來存放函數的參數,局部變量等來實作函數調用。有時候函數有嵌套調用,這個時候棧中會有多個函數的資訊,每個函數占用一個連續的區域。一個函數占用的區域被稱作幀()。同時棧是線程獨立的,每個線程都有自己的棧。例如下面簡單的函數調用:

堆棧、棧幀、函數調用、記憶體配置設定總結

另外函數堆棧的清理方式決定了當函數調用結束時由調用函數或被調用函數來清理函數幀,在VC中對函數棧的清理方式由兩種:

參數傳遞順序 誰負責清理參數占用的堆棧
__stdcall 從右到左 被調函數
__cdecl 從右到左 調用者

2) 有了上面的知識為鋪墊,我們下面細看一個函數的調用時堆棧的變化:

代碼如下:

堆棧、棧幀、函數調用、記憶體配置設定總結

Code

int Add(int x, int y)

{

    return x + y;

}

void main()

{

    int *pi = new int(10);

    int *pj = new int(20);

    int result = 0;

    result = Add(*pi,*pj);

    delete pi;

    delete pj;

}

對上面的代碼,我們分為四步,當然我們隻畫出了我們的代碼對堆棧的影響,其他的我們假設它們不存在,哈哈!

第一,int *pi = new int(10);   int *pj = new int(20);   int result = 0; 堆棧變化如下:

堆棧、棧幀、函數調用、記憶體配置設定總結

第二,Add(*pi,*pj);堆棧如下:

堆棧、棧幀、函數調用、記憶體配置設定總結

第三,将Add的結果給result,堆棧如下:

堆棧、棧幀、函數調用、記憶體配置設定總結

第四,delete pi;    delete pj; 堆棧如下:

堆棧、棧幀、函數調用、記憶體配置設定總結

第五,當main()退出後,堆棧如下,等同于main執行前,哈哈!

堆棧、棧幀、函數調用、記憶體配置設定總結

http://blog.csdn.net/zhongguoren666/article/details/7586074

函數調用是程式設計中的重要環節,也是程式員應聘時常被問及的,本文就函數調用的過程進行分析。

一、堆和棧

首先要清楚的是程式對記憶體的使用分為以下幾個區:

l         棧區(stack):由編譯器自動配置設定和釋放,存放函數的參數值,局部變量的值等。操作方式類似于資料結構中的棧。

l         堆區(heap):一般由程式員配置設定和釋放,若程式員不釋放,程式結束時可能由作業系統回收。與資料結構中的堆是兩碼事,配置設定方式類似于連結清單。

l         全局區(static):全局變量和靜态變量存放在此。

l         文字常量區:常量字元串放在此,程式結束後由系統釋放。

l         程式代碼區:存放函數體的二進制代碼。

典型的記憶體區域配置設定如圖所示:

堆棧、棧幀、函數調用、記憶體配置設定總結

其次是堆和棧的申請方式:

棧由系統自動配置設定,速度較快,在windows下棧是向低位址擴充的資料結構,是一塊連續的記憶體區域,大小是2MB。

堆需要程式員自己申請,并指明大小,速度比較慢。在C中用malloc,C++中用new。另外,堆是向高位址擴充的資料結構,是不連續的記憶體區域,堆的大小受限于計算機的虛拟記憶體。是以堆空間擷取和使用比較靈活,可用空間較大。

二、棧幀結構和函數調用過程

棧在函數調用中的作用:參數傳遞、局部變量配置設定、儲存調用的傳回位址、儲存寄存器以供恢複。

棧幀(stack Frame):一次函數調用包括将資料和控制從代碼的一個部分傳遞到另外一個部分,棧幀與某個過程調用一一映射。每個函數的每次調用,都有它自己獨立的一個棧幀,這個棧幀中維持着所需要的各種資訊。寄存器ebp指向目前的棧幀的底部(高位址),寄存器esp指向目前的棧幀的頂部(低址地)。

函數調用規則:

l         _cdecl:按從右至左的順序壓參數入棧,由調用者把參數彈出棧。由于每次函數調用都要由編譯器産生清楚堆棧的代碼,是以使用_cdecl的代碼比使用_stdcall的代碼要大很多,但是這種方式支援可變參數。對于C函數,名字修飾約定為在函數名前加下劃線。對于C++,除非特變使用extern C,C++使用不同的名字修飾方式。

l         _stdcall:按從右至左的順序壓參數入棧,由被調用者把參數彈出棧。調用約定在輸出函數名前加上一個下劃線字首,後面加上一個“@”符号和其參數的位元組數。

l         _fastcall:主要特點就是快,因為它是通過寄存器來傳送參數的,和__stdcall很象,唯一差别就是頭兩個參數通過寄存器傳送。注意通過寄存器傳送的兩個參數是從左向右的,即第一個參數進ECX,第2個進EDX,其他參數是從右向左的入stack。傳回仍然通過EAX。

最後,以一個例子來解釋函數調用過程

void func(int param1 ,int param2,int param3)

{

       int var1 = param1;

       int var2 = param2;

       int var3 = param3;

       printf("param1位址:0X%08X/n",&param1);

       printf("param2位址:0X%08X/n",&param2);

       printf("param3位址:0X%08X/n",&param3);

       printf("var1位址:  0X%08X/n",&var1);

       printf("var2位址:  0X%08X/n",&var2);

       printf("var3位址:  0X%08X/n",&var3);

}

int main(int argc, char* argv[])

{

       func(1,2,3);

       return 0;

}

運作結果如圖:

堆棧、棧幀、函數調用、記憶體配置設定總結

下面分析調用過程:

在堆棧中變量分布是從高位址到低位址分布,EBP是指向棧底的指針,在過程調用中不變,又稱為幀指針。ESP指向棧頂,程式執行時移動,ESP減小配置設定空間,ESP增大釋放空間,ESP又稱為棧指針。3個參數以從左向右的順序壓入堆棧,及從param3到param1,棧内分布如下圖:

堆棧、棧幀、函數調用、記憶體配置設定總結

然後是傳回位址入棧:此時的棧内分布如下:

堆棧、棧幀、函數調用、記憶體配置設定總結

通過跳轉指令進入函數後,函數位址入棧後,EBP入棧,然後把目前ESP的值給EBP,彙編指令如下:

push ebp

mov ebp esp

此時棧頂和棧底指向同一位置,棧内分布如下:

堆棧、棧幀、函數調用、記憶體配置設定總結

然後是    int var1 = param1; int var2 = param2; int var3 = param3;按申明順序依次存儲。

堆棧、棧幀、函數調用、記憶體配置設定總結

繼續閱讀