函數調用
計算機運行時使用函數完成命令
函數調用是計算機編或運行時,使用某個函數來完成相關命令。對無參函數調用時則無實際參數表。實際參數表中的參數可以是常數、變數或其它構造類型數據及表達式。各實參之間用逗號分隔。函數調用的一般形式加上分號即構成函數語句。應特別注意的是,無論是從左至右求值,還是自右至左求值,其輸出順序都是不變的,即輸出順序總是和實參表中實參的順序相同。在主調函數中對被調函數作說明的目的是使編譯系統知道被調函數返回值的類型,以便在主調函數中按此種類型對返回值作相應的處理。
程序函調執函,程語言子程序調用相似。
C語言,函調般形式:
函(際參)
對無參函數調用時則無實際參數表。實際參數表中的參數可以是常數、變數或其它構造類型數據及表達式。各實參之間用逗號分隔。
操作系統的進程空間可分為用戶空間和內核空間,它們需要不同的執行許可權。其中函數調用運行在用戶空間。
函數作為表達式中的一項出現在表達式中,以函數返回值參與表達式的運算。這種方式要求函數是有返回值的。例如:z=max(x,y)是一個賦值表達式,把max的返回值賦予變數z。
函數作為另一個函數調用的實際參數出現。這種情況是把該函數的返回值作為實參進行傳送,因此要求該函數必須是有返回值的。例如: printf("%d",max(x,y)); 即是把max調用的返回值又作為printf函數的實參來使用的。在函數調用中還應該注意的一個問題是求值順序的問題。所謂求值順序是指對實參表中各量是自左至右使用呢,還是自右至左使用。對此,各系統的規定不一定相同。介紹printf 函數時已提到過,這裡從函數調用的角度再強調一下。
【例】
main()
{int i=8;printf("%d\n%d\n%d\n%d\n",++i,--i,i++,i--);}
如按照從右至左的順序求值。運行結果應為:
8
7
7
8
如對printf語句中的++i,--i,i++,i--從左至右求值,結果應為:
9
8
8
9
應特別注意的是,無論是從左至右求值,還是自右至左求值,其輸出順序都是不變的,即輸出順序總是和實參表中實參的順序相同。由於Turbo C現定是自右至左求值,所以結果為8,7,7,8。上述問題如還不理解,上機一試就明白了。
被調用函數的聲明和函數原型
在主調函數中調用某函數之前應對該被調函數進行說明(聲明),這與使用變數之前要先進行變數說明是一樣的。在主調函數中對被調函數作說明的目的是使編譯系統知道被調函數返回值的類型,以便在主調函數中按此種類型對返回值作相應的處理。
其一般形式為:
類型說明符 被調函數名(類型形參,類型 形參…);
或為:
類型說明符 被調函數名(類型,類型…);
括弧內給出了形參的類型和形參名,或只給出形參類型。這便於編譯系統進行檢錯,以防止可能出現的錯誤。
例main函數中對max函數的說明為:
int max(int a,int b);
或寫為:
int max(int,int);
C語言中又規定在以下幾種情況時可以省去主調函數中對被調函數的函數說明。
1) 如果被調函數的返回值是整型或字元型時,可以不對被調函數作說明,而直接調用。這時系統將自動對被調函數返回值按整型處理。例8.2的主函數中未對函數s作說明而直接調用即屬此種情形。
2) 當被調函數的函數定義出現在主調函數之前時,在主調函數中也可以不對被調函數再作說明而直接調用。例如例8.1中,函數max的定義放在main 函數之前,因此可在main函數中省去對max函數的函數說明int max(int a,int b)。
3) 如在所有函數定義之前,在函數外預先說明了各個函數的類型,則在以後的各主調函數中,可不再對被調函數作說明。例如:
char str(int a);
float f(float b);
main()
{……}
char str(int a)
{……)
float f(float b)
{……}
其中第一,二行對str函數和f函數預先作了說明。因此在以後各函數中無須對str和f函數再作說明就可直接調用。
4) 對庫函數的調用不需要再作說明,但必須把該函數的頭文件用include命令包含在源文件前部。
C語言中不允許作嵌套的函數定義。因此各函數之間是平行的,不存在上一級函數和下一級函數的問題。但是C語言允許在一個函數的定義中出現對另一個函數的調用。這樣就出現了函數的嵌套調用。即在被調函數中又調用其它函數。這與其它語言的子程序嵌套的情形是類似的。其關係可表示如圖。
圖表示了兩層嵌套的情形。其執行過程是:執行main函數中調用a函數的語句時,即轉去執行a函數,在a函數中調用b 函數時,又轉去執行b函數,b函數執行完畢返回a函數的斷點繼續執行,a函數執行完畢返回main函數的斷點繼續執行。
【例】計算s=2∧2!+3∧2!
本題可編寫兩個函數,一個是用來計算平方值的函數f1,另一個是用來計算階乘值的函數f2。主函數先調f1計算出平方值,再在f1中以平方值為實參,調用 f2計算其階乘值,然後返回f1,再返回主函數,在循環程序中計算累加和。
long f1(int p)
{int k;
long r;
long f2(int);
k=p*p;
r=f2(k);
return r;}
long f2(int q)
{long c=1;
int i;
for(i=1;i<=q;i++)
c=c*i;
return c;}
main()
{int i;
long s=0;
for (i=2;i<=3;i++)
s=s+f1(i);
printf("\ns=%ld\n",s);}
在程序中,函數f1和f2均為長整型,都在主函數之前定義,故不必再在主函數中對f1和f2加以說明。在主程序中,執行循環程序依次把i值作為實參調用函數f1求i2值。在f1中又發生對函數f2的調用,這時是把i2的值作為實參去調f2,在f2 中完成求i2!的計算。f2執行完畢把C值(即i2!)返回給f1,再由f1返回主函數實現累加。至此,由函數的嵌套調用實現了題目的要求。由於數值很大,所以函數和一些變數的類型都說明為長整型,否則會造成計算錯誤。
EBP
EBP是所謂的幀指針,指向當前活動記錄的上方(上一個活動記錄的最下方)
ESP
ESP是所謂的棧指針,指向當前活動記錄的最下方(下一個將要插入的活動記錄的最上方)
這兩個指針的值規定了當前活動記錄的位置
將函數參數壓棧:mov eax,dword ptr [n] ;(n為參數變元)
push eax
函數調用將執行如下操作:
⒈將幀指針壓入棧中:push ebp
⒉使得幀指針等於棧指針:mov ebp,esp
⒊使棧指針自減,自減得到的內存地址應當能夠(足夠)用來存儲被調用函數的本地狀態:sub esp,0CCh
注意:0CCh為0xCC,隨著具體函數的不同而不同。
push ebx ;保存ebx寄存器的值
push esi ;保存esi寄存器的值
push edi ;保存edi寄存器的值
lea edi,[ebp-0CCh] ;0cch是當前活動記錄的大小。
EDI是目的變址寄存器。
00411417 pop edi
00411418 pop esi
pop ebx
add esp,0CCh
當函數返回時,編譯器和硬體將執行如下操作:
⒈使棧指針等於幀指針: mov esp,ebp
⒉從棧中將舊的幀指針彈出: pop ebp
⒊返回:ret
;void function(int n);{push ebp
mov ebp,esp
sub esp,0CCh
push ebx
push esi
push edi
lea edi,[ebp-0CCh]
mov ecx,33h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
;char a=1;
mov byte ptr [a],1
;if(n==0)return;
cmp dword ptr [n],0
jne function+2Ah (4113CAh)
jmp function+77h (411417h)
;printf("%d\t(0x%08x)\n",n,&n);
mov esi,esp
lea eax,[n]
push eax
mov ecx,dword ptr [n]
push ecx
push offset string "%d\t(0x%08x)\n" (415750h)
call dword ptr [__imp__printf (4182B8h)]
add esp,0Ch
cmp esi,esp
call @ILT+305(__RTC_CheckEsp) (411136h)
;function(n-1);
mov eax,dword ptr [n]
sub eax,1
push eax
call function (411041h)
add esp,4
;printf("----%d\t(0x%08x)\n",n,&n);
mov esi,esp
lea eax,[n]
push eax
mov ecx,dword ptr [n]
push ecx
push offset string "----%d\t(0x%08x)\n" (41573Ch)
call dword ptr [__imp__printf (4182B8h)]
add esp,0Ch
cmp esi,esp
call @ILT+305(__RTC_CheckEsp) (411136h);}
pop edi
pop esi
pop ebx
add esp,0CCh
cmp ebp,esp
call @ILT+305(__RTC_CheckEsp) (411136h)
mov esp,ebp
pop ebp
ret
117: bR = t1(p);
彙編代碼如下:
00401FB8 mov ecx,dword ptr [ebp-8] ;將參數放入ecx寄存器
00401FBB push
ecx
;參數入棧
00401FBC call @ILT+10(t1) (0040100f) ;函數調用,下一行地址00401FC1入棧
00401FC1 add
esp,4
;函數返回,堆棧指針加4,復原為00401FB8時的值
00401FC4 mov dword ptr [ebp-10h],eax ;從eax中取出高級語言中的函數返回值,放入bR變數中
其中t1函數如下:
125: BOOL t1(void* p)
126: {
00402030 push
ebp
;ebp入棧
00402031 mov
ebp,esp
;ebp指向此時堆棧的棧頂
00402033 sub
esp,44h
;esp減少一個值,空出一段存儲區
00402036 push
ebx
;將三個寄存器的值入棧,以便在函數中使用它
00402037 push
esi
;
00402038 push
edi
;
00402039 lea edi,[ebp-44h] ;
0040203C mov
ecx,11h
;
00402041 mov eax,0CCCCCCCCh ;
00402046 rep stos dword ptr [edi] ;
127:
int* q = (int*)p;
;
00402048 mov eax,dword ptr [ebp+8] ;ebp+8指向函數輸入參數的最低位地址;
;如果是ebp+4則指向函數返回地址00401FC1的最低位,值為C1
0040204B mov dword ptr [ebp-4],eax ;
128: return 0;
0040204E xor
eax,eax
;返回值放入eax寄存器中
129: }
00402050 pop
edi
;三個寄存器出棧
00402051 pop
esi
;
00402052 pop
ebx
;
00402053 mov
esp,ebp
;esp復原
00402055 pop
ebp
;ebp出棧,它的值也復原了
00402056 ret
;返回到此時棧頂存儲的代碼地址:00401FC1
;故而如果不幸被修改了返回地址,程序就會出現意外
以上彙編代碼由VC++6.0編譯得到。
堆棧在EBP入棧后的情況:
低位 高位
↓ ↓
內存地址 堆棧
┆ ┆
0012F600├────────┤← edi = 0012F600
│
│
0012F604├─┄┄┄ ┄─┤
│ │
│ │
┆ 44h的空間 ┆
┆ ┆
│
│
│
│
0012F640├─┄┄┄┄─┤
│ │
0012F644├────────┤← ebp被賦值后指向該單元,此時ebp=0012F644
│AC F6 12 00 │ebp賦值為esp之前的值
0012F648├────────┤
│C1 1F 40 00 │返回地址
0012F64C├────────┤← ebp + 8
│A0 F6 12 00 │函數實參p的值;
0012F650├────────┤
│ │
├────────┤
┆ ┆
註:存儲器存儲空間堆棧按從高到低的排列,左邊標註的地址是其右下方存儲單元的最低位地址。如0012F644指向0012F6AC的AC位元組,AC在棧頂。圖中存儲器中的內容按從低到高位書寫,“AC F6 12 00”= 0x0012F6AC
說明
(1)一個c程序由一個或多個程序模塊組成,每一個程序模塊作為一個源程序文件。對較大的程序,一般不希望把所有內容全放在一個文件中,而是將它們分別放在若干個源文件中,由若干個源程序文件組成一個c程序。這樣便於分別編寫和編譯,調高調試效率。一個源程序文件可以為多個c程序公用。
(2)一個源程序文件由一個或多個函數以及其他有關內容(如指令,數據聲明與定義等)組成。一個源程序文件是一個編譯單位,子啊程序編譯時是以源程序文件為單位進行編譯的,而不是以函數為單位進行編譯的。
(3)c程序的執行是從main函數開始的,如果在main函數中調用其他函數,在調用后流程返回main函數,在main函數中結束整個程序的進行。
(4)所有函數都是平行的,即在定義函數時是分別進行的,是互相獨立的。一個函數並不從屬於另一個函數,即函數不能嵌套定義。函數間可以互相調用,但不能調用main函數。main函數是被操作系統調用的。
(5)從用戶的角度來看函數分為兩種
a:庫函數,它是由系統提供的,用戶不必自己定義,可直接使用它們。應該說明,不同的c語言編譯系統提供的庫函數的數量和功能會有一些不同,當然許多基本的函數是共同的。
b:用戶自己定義的函數。它是以解決用戶專門需求的函數。
(6)從函數的形式來看,函數分為兩類。
a:無參函數。無參函數可以帶回或不帶回函數值,但一般不帶回函數值較多。
b:有參函數。在調用函數時,主調函數在調用被調函數時,通過參數向被調函數傳遞數據。一般情況下,執行調用函數時會得到一個函數值,供主調函數使用。