6.1 函数定义
在C程序设计中,将完成指定功能的C代码定义成函数,变成逻辑上一个相对独立的程序单位。函数定义需要指明函数返回值的类型、函数名、函数的形式参数(常简称形参)和函数体(包括说明和定义及语句序列)。函数定义的一般形式为
存储类型说明符 数据类型说明符 函数名(形式参数表)
形式参数说明序列
{
说明和定义部分
执行语句序列
}
存储类型说明符或省缺,或为static.省缺表示一个全局函数,static表示一个静态函数,只供同一源程序文件中的函数使用。
数据类型说明符用来指定函数返回值类型,可以是基本数据类型、某种指针类型、结构类型等。但不可以是数组类型。特别当函数不返回结果时,可用void明确指明函数不返回值。
数据类型说明符也可省缺,省缺被默认为返回int型值。
函数名是一个标识符。形式参数表是用远号分隔的若干形式参数,用不同的标识符指明各形式参数的名。形式参数说明序列用来说明各形式参数的数据类型,相同数据类型的形式参数可以一起说明。现在编写C程序的习惯是形式参数说明序列直接放在形式参数表中,即在形式参数说明表中顺序列出各形式参数的数据类型和形式参数的名称。如是这样,一般形式的第一行全部内容称为函数头,也称为函数模型。
特别情况,函数可能不设形式参数,也就没有形式参数表和形式参数说明序列。但函数名后的一对圆括号是不可以没有的。
一对花括号括住的部分称为函数体,函数体包括类型说明、变量定义和函数的执行语句序列。在函数体内可以有return语句终止函数的执行。如函数有返回值类型,则return语句中一定要有表达式,作为函数调用的返回值。
6.2 函数调用
函数被定义以后,凡要实现函数功能的地方,就可简单地通过函数调用来完成。按函数调用在程序中的作用,有两种不同类型的应用:
(1)函数调用只是利用函数所完成的功能。此时,将函数调用作为一个独立的语句。其调用的一般形式为
函数名(实际参数表);
这种应用不要求或程序不利用函数的返回值。如程序中经常调用格式输入函数scanf()和格式输出函数printf()等。
(2)函数调用是利用函数的返回值。其调用的一般形式为
函数名(实际参数表)
这种应用是利用返回值继续进行表达式的计算,或输出函数返回值等。
函数调用时提供的实际参数按它们出现的顺序与函数定义中的形式参数—一对应,并要求实际参数类型与其对应的形式参数类型相一致。一般情况下,函数调用应为函数定义中的每个形式参数提供实际参数,当有多个实际参数时,实际参数之间用逗号分隔。
函数调用的执行过程描述如下:
(l)为函数的形式参数分配内存空间;
(2)计算实际参数表达式的值,并将实际参数表达式的值赋给对应的形式参数;
(3)为函数的局部变量分配内存空间;
(4)执行函数体内的语句序列;
(5)函数体执行完成,或执行了函数体内的return语句(若return语句带表达式,则计算出该表达式的值,并以此值作为函数的返回值)后,释放为这次函数调用分配的全部内存空间;
(6)将函数值(如果有)返回到函数调用处继续执行。
下面以简单的程序例子说明函数调用的执行过程。
「例6.l」函数调用时,由实际参数向函数形式参数传递值的示意程序。
# include stdio.h
double x,y,d;
double min(double a,double b)
{double temp;
temp=a b? b :a;
return temp;
}
void main()
{ printf(“Enter x, y.\n”);
scanf(“%lf%lf”, x, y);
d=min(x,y);
printf(“MIN(%.3f,%.3f=%.3f\n”, x, y,d)
}
以上示意程序的大致执行过程如下:
首先执行主函数的第一个语句,调用格式输出函数输出提示信息。接着调用格式输入函数,等待用户输入数据。用户看到程序输出的提示信息,输入数据,输入的数据被格式输入函数所接受,并将输入数据译成内部形式后,存入变量X和y.接着执行赋值语句,求右瑞表达式的值。该表达式以x和y的值为实际参数,调用函数main()。对函数min()的调用发生时,系统先保留好控制的返回点。在执行被调用函数min()之前,先为函数的形式参数a和b分配存储单元,并以它们对应的实际参数表达式的植给它们赋初值。接着为函数内部的变量(称为局部变量)分配存储单元。之后才开始执行被调用函数体中的语句。执行完函数体的语句,或执行了return语句,函数准备返回。在返回之前先将形式参数和局部变量所占用的存储单元全部释放。函数返回时,将函数的返回值带回调用处,从原先保留的控制返回点,继续执行,将函数调用的返回值存于变量d,输出结果,结束程序。综上所述,函数调用时,系统要做许多辅助工作,函数调用时发生的数据传递最主要的是实际参数向形式参数传递数据和函数的返回值传递给调用处。为正确编写函数,实现函数调用所希望的要求,需正确了解以下几项内容:
(1)当函数执行return语句或执行完函数体的语句序列后,函数的这次调用就执行结束,随之将控制返回到函数调用处继续执行。
(2)函数的返回值是通过执行return语句时,计算return之后的表达式值而获得的。如果函数不提供返回值,则return语句不应包含表达式。
(3)如果函数有返回值,则应有确定的类型,并在函数定义时指明。同时,return语句的表达式类型应与函数定义中指明的返回值类型相一致。
(4)为了明确指明函数不提供返回值,建议在函数定义时,在函数名之前写上void.并在这样的函数体内,所有的return语句都不应该带表达式。
6.3 函数说明
C函数都是外部的,一般来说,任一函数都能被其它函数调用。而一个函数要调用另一个函数,应知道被调用函数的一些有关如何正确调用的信息。调用函数与被调用函数之间在程序正文中可能会存在以下几种情况。
(l)调用同一程序文件中前面已定义的函数。
(2)调用处于同一程序文件后面定义的函数。
(3)调用别的程序文件中定义的函数。
对于第一种情况,因在函数调用处,被调用函数的详细信息已被编译程序所接受,在函数定义之后调用前面已定义的函数,能方便地检查调用的正确性。对于后两种情况,这时因被调用函数的信息还未被编译程序所接受,不能检查函数调用的正确性,所以在调用之前需对被调用函数有关调用的一些信息作出说明。如函数的返回值类型、函数名和函数有关形式参数的个数及其类型等。这样的说明称作函数说明。函数说明的一般形式为
存储类型说明符 数据类型说明符 函数名(形式参数说明表);
其中存储类型说明符可以省缺,或写成extern.形式参数说明表可以为空,也可以顺序列出各形式参数的类型,同样也可以顺序列出各形式参数的类型和形式参数的名。
6.4 函数调用中的数据传递方式
函数调用时,调用处与被调用函数之间会有数据传递发生。在C程序中,函数调用的数据传递方式有四种:实际参数的数据值传递给形式参数(值传递方式)、实际参数的指针值传递给形式参数(地址传递方式)、函数以返回值传递给调用环境(返回值方式)、调用环境与被调用函数共用全局变量(全局变量传递方式)。在地址传递方式中,实际参数可以是传递一般变量的指针、数组某元素的指针、字符串某字符指针等。
1.实际参数向形式参数传递非指针数据
调用带形式参数的函数时,调用处将实际参数的值传递给被调用函数的形式参数。在此要特别说明以下几点:
( l)函数调用时实际参数为对应的形式参数提供初值,实际参数表达式的值是在执行函数体之前计算的,函数调用中的实际参数可以是常量、变量或是一般的表达式。
(2)C语言规定,实际参数表达式对形式参数的数据传递是“值传递”的,是单向传递。如实际参数也是变量,则实际参数变量与形式参数是不同的变量,实际参数变量的值传给形式参数,而不能由形式参数直接传回给实际参数。在函数执行过程中,形式参数变量的值可能被改变,但这改变对原先与它对应的实际参数变量没有影响。
2.实际参数向形式参数传递指针
实际参数向形式参数传递变量的指针
函数可以设置指针类型的形式参数,调用带指针类型形式参数的函数时,对应指针形式参数的实际参数必须是相同类型的指针(如胡同类型的某变量的指针),指针形式参数变量从实际参数处得到某变量的指针。指针形式参数对于函数来说有特别的作用,它使函数得到了调用环境中某变量的地址,函数就可用这个地址间接访问函数之外的变量。因此,指针类型形式参数为函数改变调用环境中的数据对象提供了手段。
如希望函数能通过形式参数改变任意指定变量的值,需要在三个方面协调一致:
(1)首先,函数应设置指针类型的形式参数;
(2)其次,函数体必须通过指针形式参数间接访问变量,或引用其值或修改其值;
(3)最后,调用函数时,要以欲改变值的变量的指针为实际参数调用函数。
实际参数向形式参数传递数组元素的指针
为了能使函数处理不同的成组变量,应向形式参数传递数组元素的指针,最通常的情况是数组首元素的指针。由于数组名能代表数组首元素的指针,所以常用数组名实际参数给形式参数传递数组首元素的指针。例如,用于求数组前n个元素和的函数sun(),这个函数被正确地设置有两个形式参数:一个形式参数是数组元素的指针;另一个整型的形式参数用于指定求和数组的元素个数。
「例6.2」求数组元素和的函数。
int sum( int *a, int n)
{ int i, s;
for(s=i=0; i n; i++)
s+=a[i];
return s;
}
利用函数sum(),如有以下变量定义:
int x[]= {1, 2, 3, 4, 5 }, i, j;
则语句
i=sum(x,5); j=sum( x[2],3);
printf(“i=%d\n j=%d\n”, i,j);
将输出:
i=15
j= 12
函数调用sum(x,5)将数组x的首元素地址( x[0])传送给形式参数a;函数调用sum( x[2], 3)将数组x的元素x[2]的地址( x[2])传送给形式参数a,而x[2]的地址就是数组元素段x[2]、x[3]、x[4] 的开始地址。
为了明确指明形式参数是数组元素的指针,形式参数的类型可以指定为数组类型的。如改写后的函数sum()定义如下:
int sum(int a[], int n)
{int i,s;
for(s= i=0; i n; i++)
s+=a[i] ;
return s;
}
对于数组类型的形式参数来说,函数被调用时,与它对应的实在数组由多少个元素是不确定的,可能会对应一个大数组,也可能会对应一个小数组,甚至会对应数组中的某一段。所以在数组形式参数说明中,形式参数数组不必指定数组元素的个数。任何数组形式参数说明:
类型 形式参数名[ ]
都可改写成:
类型 *形式参数名
函数形式参数也是函数的一种局部变量,指针形式参数就是函数的指针变量,函数sum()的定义又可改写成如下形式:
int sum(int *a, int n)
{ int s=0;
for(; n——;)
s+=*a++;
return s;
}
实际参数向形式参数传递字符串某字符的指针
这种情况要求形式参数为字符指针的,对应的实际参数是字符数组某个元素的指针,通常是字符串的首字符指针。由于字符率是用一维的字符数组来实现的,所以字符指针形式参数与指向数组元素指针形式参数有相同的使用方法。但因字符串的特殊性,在编写字符串处理函数时还会有许多技巧。下面以字符串拷贝函数strcpy()的实现为例说明字符指针形式参数的用法。
「例6.3」字符串拷贝函数strcpy()。
该函数功能是将一个已知字符串的内容复制到另一字符数组中。拷贝函数设有两个形式参数from,to.from为已知字符串的首字符指针,to为存储复制字符串首字符指针。函数定义如下:
void strcpy(char *to, char *from,)
{
while( *to++=*from++);
}
3.调用环境与函数共用全局变量
为了减少函数的形式参数,或因若干函数必须共同对一组变量进行处理。可让调用环境与被调用的函数共用一组变量。即在函数调用之前先给变量设置初值,函数对这些变量进行处理,并将处理结果留在全局变量中。由于这种使用方式函数之间相互影响太大,如程序有错,就会很难修正。
6.5 返回指针的函数
函数也可以返回指向某种数据对象的指针值。定义(或说明)返回指针值函数的函数头有以下形式:
类型说明符 * 函数名(形式参数表)
例如,函数说明:
int *f(int,int);
说明函数f()返回指向int型数据的指针,该函数有两个整型形式参数。
在函数名的两侧分别为* 运算符和()运算符,而()的优先级高于*,函数名先与()结合。函数名()是函数的说明形式。在函数名之前的* ,表示此函数返回指针类型的值。
「例6.4」 编制在给定的字符串中找特定字符的第一次出现。若找到,返回指向字符串中该字符的指针;否则,返回NULL值。
设函数为search(),该函数有两个形式参数,指向字符串首字符的指针和待寻找的字符。以下是函数search()的定义:
char *search(char *s,char c)
{ while(*s *s! = c)
s++;
return *s?s:NULL;
}
6.6 函数递归调用
一个函数为完成它的复杂工作,可以调用其它别的函数。例如,从主函数出发,主函数调用函数A() ,函数A()又调用函数B(),函数B()又调用函数C(),等等。这样从主函数出发,形成一个长长的调用链,就是通常所说的函数嵌套调用。函数嵌套调用时,有一个重要的特征:先被调用的函数后返回。如这里所举例子,待函数C()完成计算返回后,B()函数继续计算(可能还要调用其它函数) ,待计算完成,返回到函数A(),函数A()计算完成后,才返回到主函数。
当函数调用链上的某两个函数为同一个函数时,称这种函数调用方式为递归调用。通过速归调用方式完成其功能的函数称为递归函数。许多问题的求解方法具有递归特征,用递归函数描述这种求解算法比较简洁。计算n的阶乘(n!)函数就是一个很好的例子。因
n! = l*2*3* …*n
按其定义用循环语句可以方便地实现,写成函数见下例6.5.
「例6.5」用循环实现阶乘计算的函数。
float fac(int n)
{float s;
int i;
for(s=1.of,i=l;i =n; i++)
s*= l;
return s;
}
然而,把n! 的定义改写成以下递归定义形式
(l)n!=1, n =l;
(2)n!= n*(n-1)!, n l.
根据这个定义形式可用递归函数描述如下例6.6.
「例6.6」 用递归实现阶乘计算的函数。
float rfac(int n)
{
if( n =1) return 1.0f;
return n*rfac(n-1) ;
}
以计算3! 为例,说明递归函数被调用时的执行过程。设有代码m= rfac(3) 调用函数rfac()。函数调用rfac(3) 的计算过程可大致叙述如下:
以函数调用rfac(3) 去调用函数rfac() ;函数rfac(n=3) 为计算3*2! ,用rfac(2) 去调用函数rfac();函数rfac(n=2) 为计算2*1!,用rfac(1)去调用函数rfac();函数 rfac(n=l) 计算1! ,以结果1.0返回;返回到发出调用rfac(l) 处,继续计算,得到2! 的结果2.0返回;返回到发出调用rfac(2) 处,继续计算得到3! 的结果6.0返回。
递归计算n! 有一个重要特征,为求n有关的解,化为求n-l的解,求n-1的解又化为求n-2的解,如此类推。特别地,对于1的解是可立即得到的。这是将大问题解化为小问题解的递推过程。有了1的解以后,接着是一个回溯过程,逐步获得2的解,3的解,……,直至n的解。
「例6.7」 用递归函数实现数组元素的求和计算。
要采用递归方法计算数组元素的和,可把数组元素的累计和等于当前元素与数组其余元素的和,而对数组其余元素的和通过递归实现。下面的函数定义是这样的解法之一。
int rsum(int *a, int n)
{
if( n==0) return 0;/*若数组没有元素,则返回0*/
return *a+rsum(a+l,n-1);/*当前元素与其余元素的和*/
}