一、局部變量與全局變量
函數中出現的變量可以分為局部變量和全局變量,在函數內部定義的變量(沒有global語句)就是局部變量,只有在函數內部才能夠使用它們。在函數外定義的變量就是全局變量
全局變量的作用是增加了函數間數據聯系的渠道,全局變量在全部執行過程中都占用存儲單元,如果在同一個源文件中,局部變量和全局變量同名,則在局部變量的作用范圍內全局變量被屏蔽即它不起作用。
靜態局部變量,有時希望局部變量的值在函數調用結束后不消失而保持原值,即其占用的存儲空間不釋放,在下一次函數調用時,該變量已有值,即上次函數調用結束時的值,就應該指定該局部變量為"靜態局部變量",用static聲明。靜態局部變量屬於靜態存儲類別,在靜態存儲區分配內存單元,在程序整個運行期間都不釋放,動態局部變量屬於動態存儲類別,站動態存儲區,函數調用結束即釋放。靜態局部變量的賦值是在編譯期,即只賦值一次,在程序運行時它已有初值,以后每次調用函數不再重新賦值而是保留上次函數調用結束的值,而對動態局部變量不是在編譯時期進行的,而是在函數調用時進行的,每調用一次函數就重新給一次賦值
二、函數調用過程的分析
1、返回地址的存儲
執行一條指令時,是根據PC中存放的指令地址,將指令由內存取到指令寄存器IR中。程序在執行時按順序依次執行每一條語句,PC通過加1來指向下一條將要執行的程序語句。但也有一些例外:(1)調用函數(2)函數調用后的返回(3)控制結構(if else while for等)
主調函數是指調用其他函數的函數,被調函數是指被其他函數調用的函數,一個函數既調用別的函數又被另外的函數調用
上圖中,fun0函數調用fun1,fun0函數就是主調函數,fun1是被調函數
發生函數調用時,程序會跳轉到被調函數的第一條語句,然后按順序依次執行被調函數中的語句。函數調用后返回時,程序會返回到主調函數中調用函數的語句的后一條語句繼續執行。換句話說,也就是“從哪里離開,就回到哪里”。
CPU執行程序時,並不知道整個程序的執行步驟是怎樣的,完全是“走一步,看一步”。前面我們提到過,CPU都是根據PC中存放的指令地址找到要執行的語句。函數返回時,是“從哪里離開,就回到哪里”。但是當函數要從被調函數中返回時,PC怎么知道調用時是從哪里離開的呢?答案就是——將函數的“返回地址”保存起來。因為在發生函數調用時的PC值是知道的。在主調函數中的函數調用的下一條語句的地址即為當前PC值加1,也就是函數返回時需要的“返回地址”。我們只需將該返回地址保存起來,在被調函數執行完成后,要返回主調函數中時,將返回地址送到PC。這樣,程序就可以往下繼續執行了。
函數調用的特點是:越早被調用的函數,越晚返回。比如fun1函數比fun2函數先調用,但是返回的時候fun1晚於fun2返回。這一特點正是"后進先出",所以我們采用棧來保存返回地址
如上圖調用過程(1)發生時,需要壓入保存返回地址A,棧的狀態如圖中(a)所示;調用過程(2)發生時,需要壓入保存返回地址B,棧的狀態如圖中(b)所示;返回過程(3)發生時,需要彈出返回地址B,棧的狀態如圖中(c)所示;調用過程過程(4)發生時,需要壓入保存返回地址C,棧的狀態如圖中(d)所示;返回過程(5)發生時,需要彈出返回地址C,棧的狀態如圖中(e)所示;返回過程(6)發生時,需要彈出返回地址A,此時棧被清空,圖中未畫出具體情況
2、函數調用時棧的管理
如上圖所示,fun函數里的變量a和do_add函數里的變量a是兩個不同的變量,這兩個變量需要存放在不同的地方。局部變量a只在do_add函數內才有意義;局部變量的存儲一定是和函數的開始與結束息息相關的。局部變量如同返回地址般也是存在棧里。當函數開始執行時,這個函數的局部變量在棧里被設立(壓入),當函數結束時,這個函數的局部變量和返回地址都會被彈出。
當函數調用時,do_add函數里局部變量c就復制fun函數里變量a的值。在函數返回時,與參數傳遞同理,在傳遞返回值時也是將do_add函數里的值賦值給主調函數中的變量b。局部變量只在函數內有意義,離開函數后該局部變量就失效。比如do_add函數里的局部變量d,執行do_add函數時d是有意義的。但執行完do_add函數后,返回到fun函數中,do_add函數里的局部變量d就失效了。因此在彈出d時需要用一個寄存器將返回值d保存起來,所以在外面的調用函數可以來讀取這個值。
局部變量的調用是和棧的操作模式“后進先出”的形式是相同的。這就是為什么返回地址是壓入棧里,同樣的,局部變量也會壓到相對應的棧里面。當函數執行時,這個函數的每一個局部變量就會在棧里有一個空間。在棧中存放此函數的局部變量和返回地址的這一塊區域叫做此函數的棧幀(frame)。當此函數結束時,這一塊棧幀就會被彈出。
調用do_add()函數前執行的操作:(1)fun的局部變量a壓入棧中,其值為10(2)局部變量b壓入棧中,由於b的值還未知,因此先為b預留空間
調用do_add()函數時執行的操作:(1)返回地址壓到棧中(2)局部變量c的值10壓入棧中(c的值是通過復制fun函數中變量a得到的)(3)壓入do_add中的局部變量a,其值為3(4)執行a+c,其中a=3,c=10,相加后得d的值為13
do_add()函數返回時執行的操作:(1)do_add()函數執行完后,依次彈出do_add()的局部變量,由於需要將d的值返回,因此在彈出d的時候需要一個寄存器將返回值d保存起來(2)彈出返回地址,將返回地址傳到PC(3)返回到fun函數,fun中的局部變量b的值即為do_add()中的返回值d,此時將寄存器中的值賦給b。
在函數調用時,用一個寄存器將棧頂地址保存起來,稱為棧頂指針SP。另外還有一個幀指針FP,用來指向棧中函數信息的底端。這樣,棧就被分成了一段一段的空間。每個棧幀對應一次函數調用,在棧幀中存放了前面介紹的函數調用中的返回地址、局部變量值等。每次發生函數調用時,都會有一個棧幀被壓入棧的最頂端;調用返回后,相應的棧幀便被彈出。當前正在執行的函數的棧幀總是處於棧的最頂端。
由於函數調用時,要不斷的將一些數據壓入棧中,SP的位置是不斷變化的,而FP的位置相對於局部變量的位置是確定的,因此函數的局部變量的地址一般通過幀指針FP來計算,而非棧指針SP。
綜合前面所講,可以總結出:(1)一個函數調用過程就是將數據(包括參數和返回值)和控制信息(返回地址等)從一個函數傳遞到另一個函數。(2)在執行被調函數的過程中,還要為被調函數的局部變量分配空間,在函數返回時釋放這些空間。這些工作都是由棧來完成的。所傳參數的地址可以簡單的從FP算出來。下圖展示了棧幀的通用結構
三、實例分析
舉一個下圖中例子來綜合研究一下函數調用時對棧的管理
pre函數調用fac(1)函數前執行的操作:
(1)pre的局部變量m壓入棧中,其值為1
(2)局部變量f壓入棧中,由於f的值還未知,因此先為f預留空間
pre函數調用fac(1)函數時執行的操作:
(1)返回地址壓入棧中;
(2)fac(1)的局部變量n壓入棧中,其值為1;
(3)局部變量r壓入棧,由於r的值還未知,因此先為r預留出空間
fac(1)函數調用fac(0)時執行的操作:
(1)返回地址壓入棧中;
(2)fac(0)的局部變量n壓入棧中,其值為0;
(3)此時遞歸達到了終止條件(n==0),結束遞歸,局部變量r壓入棧,r的值為1。
fac(0)函數返回時執行的操作
(1)fac(0)函數執行完后,依次彈出fac(0)的局部變量。在彈出r時用一個寄存器將返回值r保存起來;
(2)彈出返回地址,將返回地址傳到PC;
(3)SP=FP,令SP指回fac(1)棧幀的頂部,令FP指回fac(1)棧幀的底部
(4)繼續執行函數fac(1),fac(1)中的局部變量r的值即為fac(0)中的返回值乘以n
fac(1)函數返回時執行的操作
(1)fac(1)函數執行完后,依次彈出fac(1)的局部變量,在彈出r時用一個寄存器將返回值r保存起來;
(2)彈出返回地址,將返回地址傳回到PC;
(3)SP=FP,令SP指回pre棧幀的頂部,令FP指回pre棧幀的底部
(4)繼續執行函數pre,pre中的局部變量f的值即為fac(1)中的返回值r,此時將寄存器中的值賦值給f
各類微處理器對函數調用的處理方式會有所差異,同一體系結構中對不同語言的函數調用的處理方式也會有少許的差異。但通過棧存儲局部變量和返回地址等信息,這一點是共同的。我們不需要對函數調用中的每一個執行的細節都了解清楚:知道每一次函數調用對應一個棧幀,棧幀中包含了返回地址、局部變量值等信息。還有一點要注意,解釋性語言中發生函數調用時所建立的棧,不是編譯時建立的(像C語言等是在編譯時就建好了棧),是在有需要的時候再建立的。