函數調用棧與活動記錄
在調試的時候經常遇到棧溢出,由此總結了下函數調用棧的知識。
為了了解C++是如何執行函數調用的,先考慮一個稱為棧(stack)的資料結構。棧是一種後入先出的資料結構——壓入(插入)棧的最後一項,是從棧中彈出(移走)的第一項。
函數調用棧是“在幕後起作用的”,它支援函數調用/傳回機制。它還支援每個被掉函數的自動變量的建立、維護和銷毀。
當調用每個函數時,它可能調用其他函數,而後者可能進而調用另外的函數,但所有的調用都是在傳回前進行的。最終,每個函數都必須将控制傳回給調用它的那個函數。
是以必須以某種方式跟蹤每個函數的傳回位址,以便将控制傳回到它的調用者。函數調用棧是處理這個資訊的絕佳資料結構。每次調用函數時,就會将一個項壓入棧中。這個項稱為棧幀(stack frame)或活動記錄,它包含被調用函數傳回到調用函數時所需的傳回位址。如果被調函數傳回,則會彈出這個函數調用的幀棧,且控制會轉移到被彈出的幀棧所包含的傳回位址。
調用棧的亮點在于每個被調函數都能夠在調用棧的頂部找到傳回到它的調用者時所需要的資訊。而且,如果一個函數調用了另一個函數,則這個新函數的棧幀也會被簡單地壓入調用棧。是以,新的被調函數傳回到它的調用者所需要的傳回位址,就位于棧的頂部。
幀棧還有另外一個重要責任。大多數函數都有自動變量,包括參數及他說聲明的所有局部變量。自動變量需要在函數執行時存在。如果函數調用了其他函數,則他們仍然需要保持活動狀态。但是當被調函數傳回到他的調用者後,它的自動變量需要“消失”。被調函數的幀棧是儲存它的自動變量的理想場所。隻要被調函數處于活動狀态,它的幀棧就會存在。當函數傳回時(此時不在需要它的局部自動變量),它的幀棧就從棧彈出,而這些局部變量不再為程式所知。
當然,計算機中的記憶體容量是有限的,是以隻要一定數量的記憶體能夠用于在函數調用棧上儲存活動記錄。如果發生的函數調用超出了函數調用棧上能容納的活動記錄,就會發生棧溢出(stack overflow)的錯誤。
實際使用函數調用棧
調用棧和活動記錄支援函數調用/傳回機制,也支援自動變量的建立和銷毀。
下面以一個demo說明調用棧如何支援main所調用的square函數的操作(見圖1第08-14行)。
首先:作業系統調用main,會将這一活動壓入棧中(如下圖1所示)。這個活動記錄會告訴main函數如何傳回到作業系統(即轉移到傳回位址R1) ,并包含main的自動變量(即初始化為10的a)所需的空間。
#include "stdafx.h"
#include <iostream>
using namespace std;
int square(int);
int _tmain(int argc, _TCHAR* argv[])
{
int a = 10;
cout << a << " squared: " << square(a) << endl;
system("pause");
return 0;
}
int square(int x)
{
return x*x;
}
在傳回作業系統之前,現在main函數在代碼第11行調用square函數。這會導緻square的一個幀(16~19行)(見圖2)被壓入函數調用棧(見圖2)這個棧幀包含傳回位址(即R2),使square函數可以傳回到main函數,它還包括square的自動變量(即x)所需的記憶體。
在square計算出實參的平方之後,它需要傳回到main,并且不再需要它的自動變量x的記憶體。是以,棧被彈出,向square提供main中的傳回位置(即R2),并丢失square的自動變量,(圖3)展示了square的活動記錄被彈出之後的函數調用棧。
現在,main函數顯示了調用square的結果(第11行),然後x執行return語句(13行)。這會使main的活動記錄從棧彈出。它向main提供了傳回到作業系統的位址(圖1中的R1),并使的main的自動變量(即a)的記憶體不能再通路。
參考資料:摘自c++程式員教程 P138-P141 電子工業出版社 張良華 譯