複製鏈接
請複製以下鏈接發送給好友

函數調用

鎖定
計算機編譯或運行時,使用某個函數來完成相關命令。對無參函數調用時則無實際參數表。實際參數表中的參數可以是常數、變量或其它構造類型數據及表達式。各實參之間用逗號分隔。
中文名
函數調用
外文名
function reference
一般形式
在程序中通過對函數的調用來
包括內容
函數表達式
嵌套調用
C語言中不允許作嵌套的函數定義

函數調用一般形式

在程序中通過對函數的調用來執行函數體,其過程與其它語言的子程序調用相似。
C語言中,函數調用的一般形式為:
函數名(實際參數表)
對無參函數調用時則無實際參數表。實際參數表中的參數可以是常數、變量或其它構造類型數據及表達式。各實參之間用逗號分隔。 [1] 

函數調用用户空間(用户態)和內核空間(內核態)

操作系統的進程空間可分為用户空間和內核空間,它們需要不同的執行權限。其中函數調用運行在用户空間。 [1] 

函數調用包括內容

函數調用函數表達式

函數作為表達式中的一項出現在表達式中,以函數返回值參與表達式的運算。這種方式要求函數是有返回值的。例如:z=max(x,y)是一個賦值表達式,把max的返回值賦予變量z。 [1] 

函數調用函數語句

函數調用的一般形式加上分號即構成函數語句。例如: printf ("%d",a);scanf ("%d",&b);都是以函數語句的方式調用函數。 [1] 

函數調用函數實參

函數作為另一個函數調用的實際參數出現。這種情況是把該函數的返回值作為實參進行傳送,因此要求該函數必須是有返回值的。例如: printf("%d",max(x,y)); 即是把max調用的返回值又作為printf函數的實參來使用的。在函數調用中還應該注意的一個問題是求值順序的問題。所謂求值順序是指對實參表中各量是自左至右使用呢,還是自右至左使用。對此,各系統的規定不一定相同。介紹printf 函數時已提到過,這裏從函數調用的角度再強調一下。 [1] 
【例】
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。上述問題如還不理解,上機一試就明白了。 [1] 
被調用函數的聲明和函數原型
在主調函數中調用某函數之前應對該被調函數進行説明(聲明),這與使用變量之前要先進行變量説明是一樣的。在主調函數中對被調函數作説明的目的是使編譯系統知道被調函數返回值的類型,以便在主調函數中按此種類型對返回值作相應的處理。 [1] 
其一般形式為:
類型説明符 被調函數名(類型 形參,類型 形參…);
或為:
類型説明符 被調函數名(類型,類型…);
括號內給出了形參的類型和形參名,或只給出形參類型。這便於編譯系統進行檢錯,以防止可能出現的錯誤。
main函數中對max函數的説明為:
int max(int a,int b);
或寫為:
int max(int,int);
C語言中又規定在以下幾種情況時可以省去主調函數中對被調函數的函數説明。 [1] 
1) 如果被調函數的返回值是整型或字符型時,可以不對被調函數作説明,而直接調用。這時系統將自動對被調函數返回值按整型處理。例8.2的主函數中未對函數s作説明而直接調用即屬此種情形。 [1] 
2) 當被調函數的函數定義出現在主調函數之前時,在主調函數中也可以不對被調函數再作説明而直接調用。例如例8.1中,函數max的定義放在main 函數之前,因此可在main函數中省去對max函數的函數説明int max(int a,int b)。 [1] 
3) 如在所有函數定義之前,在函數外預先説明了各個函數的類型,則在以後的各主調函數中,可不再對被調函數作説明。例如:
char str(int a);
float f(float b);
main()
{……}
char str(int a)
{……)
float f(float b)
{……}
其中第一,二行對str函數和f函數預先作了説明。因此在以後各函數中無須對str和f函數再作説明就可直接調用。 [1] 
4) 對庫函數的調用不需要再作説明,但必須把該函數的頭文件用include命令包含在源文件前部。 [1] 

函數調用嵌套調用

C語言中不允許作嵌套的函數定義。因此各函數之間是平行的,不存在上一級函數和下一級函數的問題。但是C語言允許在一個函數的定義中出現對另一個函數的調用。這樣就出現了函數的嵌套調用。即在被調函數中又調用其它函數。這與其它語言的子程序嵌套的情形是類似的。其關係可表示如圖。 [1] 
圖表示了兩層嵌套的情形。其執行過程是:執行main函數中調用a函數的語句時,即轉去執行a函數,在a函數中調用b 函數時,又轉去執行b函數,b函數執行完畢返回a函數的斷點繼續執行,a函數執行完畢返回main函數的斷點繼續執行。 [1] 
【例】計算s=2∧2!+3∧2!
本題可編寫兩個函數,一個是用來計算平方值的函數f1,另一個是用來計算階乘值的函數f2。主函數先調f1計算出平方值,再在f1中以平方值為實參,調用 f2計算其階乘值,然後返回f1,再返回主函數,在循環程序中計算累加和。 [1] 
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返回主函數實現累加。至此,由函數的嵌套調用實現了題目的要求。由於數值很大,所以函數和一些變量的類型都説明為長整型,否則會造成計算錯誤。 [1] 

函數調用實際實現

函數調用指針寄存器

EBP
EBP是所謂的幀指針,指向當前活動記錄的上方(上一個活動記錄的最下方)
ESP
ESP是所謂的棧指針,指向當前活動記錄的最下方(下一個將要插入的活動記錄的最上方)
這兩個指針的值規定了當前活動記錄的位置 [1] 

函數調用參數傳遞

將函數參數壓棧:mov eax,dword ptr [n] ;(n為參數變元)
push eax [1] 

函數調用操作

函數調用將執行如下操作:
⒈將幀指針壓入棧中:push ebp
⒉使得幀指針等於棧指針:mov ebp,esp
⒊使棧指針自減,自減得到的內存地址應當能夠(足夠)用來存儲被調用函數的本地狀態:sub esp,0CCh
注意:0CCh為0xCC,隨着具體函數的不同而不同。

函數調用傳入保存狀態

push ebx ;保存ebx寄存器的值
push esi ;保存esi寄存器的值
push edi ;保存edi寄存器的值 [1] 

函數調用裝入edi

lea edi,[ebp-0CCh] ;0cch是當前活動記錄的大小。
EDI是目的變址寄存器。 [1] 

函數調用恢復傳入的保存狀態

00411417 pop edi
00411418 pop esi
pop ebx [1] 

函數調用棧指針上移,恢復空間

add esp,0CCh

函數調用函數返回釋放空間

當函數返回時,編譯器和硬件將執行如下操作:
⒈使棧指針等於幀指針: mov esp,ebp [1] 
⒉從棧中將舊的幀指針彈出: pop ebp [1] 
⒊返回:ret [1] 

函數調用實例一

;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 [1] 

函數調用實例二

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 [1] 
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
;故而如果不幸被修改了返回地址,程序就會出現意外 [1] 
以上彙編代碼由VC++6.0編譯得到。
堆棧在EBP入棧後的情況: [1] 
低位 高位
↓ ↓
內存地址 堆棧
┆ ┆
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├────────┤
│ │
├────────┤
┆ ┆ [1] 
注:存儲器存儲空間堆棧按從高到低的排列,左邊標註的地址是其右下方存儲單元的最低位地址。如0012F644指向0012F6AC的AC字節,AC在棧頂。圖中存儲器中的內容按從低到高位書寫,“AC F6 12 00”= 0x0012F6AC [1] 
説明
(1)一個c程序由一個或多個程序模塊組成,每一個程序模塊作為一個源程序文件。對較大的程序,一般不希望把所有內容全放在一個文件中,而是將它們分別放在若干個源文件中,由若干個源程序文件組成一個c程序。這樣便於分別編寫和編譯,調高調試效率。一個源程序文件可以為多個c程序公用。 [1] 
(2)一個源程序文件由一個或多個函數以及其他有關內容(如指令,數據聲明與定義等)組成。一個源程序文件是一個編譯單位,子啊程序編譯時是以源程序文件為單位進行編譯的,而不是以函數為單位進行編譯的。 [1] 
(3)c程序的執行是從main函數開始的,如果在main函數中調用其他函數,在調用後流程返回main函數,在main函數中結束整個程序的進行。 [1] 
(4)所有函數都是平行的,即在定義函數時是分別進行的,是互相獨立的。一個函數並不從屬於另一個函數,即函數不能嵌套定義。函數間可以互相調用,但不能調用main函數。main函數是被操作系統調用的。 [1] 
(5)從用户的角度來看函數分為兩種
a:庫函數,它是由系統提供的,用户不必自己定義,可直接使用它們。應該説明,不同的c語言編譯系統提供的庫函數的數量和功能會有一些不同,當然許多基本的函數是共同的。 [1] 
b:用户自己定義的函數。它是以解決用户專門需求的函數。 [1] 
(6)從函數的形式來看,函數分為兩類。 [1] 
a:無參函數。無參函數可以帶回或不帶回函數值,但一般不帶回函數值較多。 [1] 
b:有參函數。在調用函數時,主調函數在調用被調函數時,通過參數向被調函數傳遞數據。一般情況下,執行調用函數時會得到一個函數值,供主調函數使用。 [1] 
參考資料
  • 1.    (美)Randal E.Bryant / David O'Hallaron .深入理解計算機系統:機械工業出版社,2010年