一般形式
在程式中通過對函式的調用來執行函式體,其過程與其它語言的子程式調用相似。
C語言中,函式調用的一般形式為:
函式名(實際參數表)
對無參函式調用時則無實際參數表。實際參數表中的參數可以是常數、變數或其它構造類型數據及表達式。各實參之間用逗號分隔。
用戶空間(用戶態)和核心空間(核心態)
作業系統的進程空間可分為用戶空間和核心空間,它們需要不同的執行許可權。其中函式調用運行在用戶空間。
包括內容
函式表達式
函式作為表達式中的一項出現在表達式中,以函式返回值參與表達式的運算。這種方式要求函式是有返回值的。例如:z=max(x,y)是一個賦值表達式,把max的返回值賦予變數z。
函式語句
函式調用的一般形式加上分號即構成函式語句。例如: printf ("%d",a);scanf ("%d",&b);都是以函式語句的方式調用函式。
函式實參
函式作為另一個函式調用的實際參數出現。這種情況是把該函式的返回值作為實參進行傳送,因此要求該函式必須是有返回值的。例如: 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暫存器的值
裝入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:有參函式。在調用函式時,主調函式在調用被調函式時,通過參數向被調函式傳遞數據。一般情況下,執行調用函式時會得到一個函式值,供主調函式使用。