一般形式
在程式中通过对函式的调用来执行函式体,其过程与其它语言的子程式调用相似。
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:有参函式。在调用函式时,主调函式在调用被调函式时,通过参数向被调函式传递资料。一般情况下,执行调用函式时会得到一个函式值,供主调函式使用。












