在有参数函数的调用时,存在一个实参与形参间参数传递。在函数未被调用时,函数的形参并不占有实际的存储单元,也没有实际值。只有当函数被调用时,系统才为形参分配存储单元,并完成实参与形参的数据传递。
图6.3 函数调用的整个执行过程
从图6.3可知,函数调用的整个执行过程分成4步:
1)创建形参变量,为每个形参变量建立相应的存储空间。
2)值传递,即将实参的值复制到对应的形参变量中。
3)执行函数体,执行函数体中的语句。
4)返回(带回函数值、返回调用点、撤消形参变量)。
函数调用的整个执行过程按上述四步依次完成。其中第2步是完成把实参的值传给形参。虽然函数调用时,都是实参的值复制给形参变量,但不同的实参数据对主调函数、被调函数的影响不尽相同。C语言中函数间的参数传递有两种:一种是传数值(即传递基本类型的数据,结构体类型数据等,而非地址数据);另一种是传地址(即传递存储单元的地址)。
注释:① 结构体是一种构造型数据类型,有关结构体数据在第9章中介绍;② 指针是存储单元的地址,有关指针类型在第8章中介绍。
6.3.2 传数值
传数值,即函数调用时实参的值是基本数据类型、结构体类型数据。实参可以是常量、变量或表达式,其值的类型是整型、实型、字符型、数组元素等数据而不能是数组名或指针等数据。当函数调用时,先为形参分配独立的存储空间,同时将实参的值赋值给形参变量。因此,在函数体执行中,若对形参变量的任何改变都不会改变实参的值。
例6.7 编函数,对末尾数非0的正整数求它的逆序数,如:reverse(3407)=7043。在主函数中输入正整数。
/* 例6.7源程序,求逆序数。 */
#include <stdio.h>
void main()
{
long a, reverse(long);
scanf("%ld",&a);
printf("调用reverse前:a=%ld\n",a);
printf("函数值:%ld\n", reverse(a));
printf("reverse后:a=%ld\n",a);
}
long reverse(long n)
{
long k=0;
while(n){
k=k*10+n%10;
n/=10;
}
return k;
}
程序执行:
37082↙
调用reverse前:a=37082
函数值:28073
调用reverse后:a=37082
程序说明:调用reverse函数时,实参a的值37 082传给形参变量n,在reverse函数执行中,形参n值不断改变,最终成0,但并没有使实参a的值随之改变。形参变量和实参变量它们各自是独立的变量,占有不同的存储空间,在函数reverse中对形参的更新,只是对形参本身进行,与实参无关,不论形参名与实参名是否相同都不影响实参值。
例6.8 读下面的程序,分析函数调用前后的实参值与形参的值。
/* 例6.8源程序,分析函数调用前后实参值与形参的值。 */
#include<stdio.h>
void main()
{
double a,z;
double mult(double);
a=5.2;
printf("调用mult前:a=%.2lf\n",a );
z=mult(a);
printf("调用mult后:a=%.2lf\n",a );
printf("z=%lf\n",z);
}
double mult(double a )
{
a=a*a;
printf("在mult中:a=%.2lf\n",a );
return (a);
}
程序执行:
调用mult前:a=5.20
在mult中:a=27.04
调用mult后:a=5.20
z=27.04
程序说明:调用mult函数时,实参a的值5.2传给形参变量a,在函数mult执行中,形参a的值被改变为27.04,并没有使实参a的值改变。形参和实参虽然变量名相同,但它们各自是独立的变量,所以形参的改变,不影响实参值。
例6.9 考察下面的swap函数,是否能完成交换主调函数中两个变量值。
/* 例6.9源程序,swap函数。 */
#include <stdio.h>
void main()
{
double x=4.5,y=7.3;
void swap( double,double ); // swap函数的声明
swap( x,y );
printf("In mian:x=%.2lf y=%.2lf\n",x,y);
}
void swap(double x,double y) // 定义交换变量值函数
{
double temp;
temp=x;
x=y;
y=temp;
printf("In swap:x=%.2lf y=%.2lf\n",x,y);
}
程序执行:
In swap: x=7.3 y=4.5
In main: x=4.5 y=7.3
程序说明:这个swap函数无法真正实现两个变量值的交换。函数调用时,当实参传给形参后,函数内部实现了两个形参变量x、y值的交换,但由于实参变量与形参变量是各自独立的(名字相同),因此实参值并没有被交换。如图6.4所示调用函数swap整个执行过程的四步骤。
从图6.4中,读者也可看到函数返回后,主函数main中的变量x、y值没有改变。
图6.4 swap函数整个调用执行过程的四步骤
事实上,值传递若是传数值方式,则被调函数是无法改变主调函数中的变量值。如何解决该问题呢?将在稍后的第8章指针中进一步讨论、解决。
传数值的两个特点:
1)参数是非指针类型。
2)被调函数无法引用主调函数中的任何变量值。
如果被调函数中要修改主调函数中的变量值,函数调用时必须采用传地址的方式。
6.3.3 传地址
传地址,实参值是指针类型的数据。实参可以是常量、变量或表达式,但实参值必须是存储单元的地址,而不能是基本数据。当函数调用时,实参值,也就是主调函数中存储单元的地址传给形参变量。由于形参变量获得的是主调函数中变量的地址,在函数体中可以通过地址,访问相应的变量,而达到改变主调函数中的变量值。
采用传地址方式时,函数定义中的形参可以是数组作为形参或指针变量作为形参。
在第5章数组中,读者已经理解数组的概念。数组是内存中的一块存储区域,数组名表示这快存储区域的首地址,通过首地址可以实现对数组中各元素的访问。数组作为函数的参数,其本质是把数组的首地址传给形参,使形参数组与实参数组成为同一个数组,使用同一块存储区域,即形参数组的存储区域就是实参数组的存储区域。因此在被调函数中对形参数组的访问,就是对主调函数中数组的访问,而达到引用主调函数中数组元素。
1)一维形参数组定义的一般形式:“类型标识符 数组名[ ],int n”。
函数调用时,若实参是一个数组名,则形参数组与实参数组共享同一个数组。形参n用来指定要处理的数组元素个数。
例6.10 编写排序函数,将数组中的n个整数,按值从小到大排序。
程序设计分析:定义函数时,需有一个形参数组“存储”被排序的n个数,和被排序的数据个数。
/* 例6.10源程序,排序函数。 */
#include<stdio.h>
void main()
{
int b[10]={1,6,7,0,8,4,3,2,9,5};
int i;
void sort(int a[ ],int n) ;
printf("排序前:\n");
for(i=0;i<10;i++)
printf("%3d",b[i]);
sort(b,10);
printf("\n排序后:\n");
for(i=0;i<10;i++)
printf("%3d",b[i]);
}
void sort(int a[ ],int n)
{
int i,k,t,j;
for(i=0; i<n-1; i++){
k=i;
for( j=i+1; j<n; j++)
if (a[k]>a[j]) k=j;
t=a[k];
a[k]=a[i];
a[i]=t;
}
}
程序执行:
Numbers:
1 6 7 0 8 4 3 2 9 5
The sorted number:
0 1 2 3 4 5 6 7 8 9
程序说明:函数sort中定义了形参int a[ ],表示一维数组作函数的参数。当函数调用时,实参是数组名,将数组b区域的首地址传给形参数组a,使形参数组a与实参数组b是同一个数组。在函数体执行时,对数组a的操作,实际就是对主调函数中实参数组b的操作。
上例sort函数中的形参n用来存放实参传来的元素个数,这样使排序函数具有灵活性,即可以指定对数组中的前n个元素进行排序。
编程思考:若将上例的函数调用语句“sort(b,10)”改为“sort(b,5);”,运行结果如何?
编程提醒:一维形参数组定义时,方括号中的长度是不起任何作用可以缺省。形参数组的真正含义将在第8章指针中进一步讨论。
例6.11 编函数计算多项式a0+a1x+a2x2+a3x3+…+anxn的值。调用函数求多项式x3+2x2+5x+3的值,其中x值从键盘输入。
/* 例6.11源程序,计算多项式函数。 */
#include<stdio.h>
double polynomial(double a[ ],int n, double x ) // 定义求多项式值的函数
{
int i;
double y,t;
y=0;
t=1;
for(i=0;i< n; i++){
y+=a[i]*t;
t*=x;
}
return y;
}
void main()
{
double b[4]={3,5,2,1},x;
printf("please input x:\n");
scanf("%lf",&x);
printf("%f", polynomial(b, 4, x));
}
程序执行:
please input x:
3.4↙
82.42
程序说明:多项式的系数存放在一维数组b中。通过数组作为函数的参数,函数调用时,使形参数组a与实参数组b共用同一个数组。
2)二维形参数组定义的一般形式:“类型标识符 数组名[ ][长度],int n,int m”。
定义二维形参数组时,必须指明第二维的长度,且长度必须与对应的实参数组的二维长度保持一致。形参n、m则指定函数中对二维数组处理时的行数和列数。
例6.12 编写函数,将5×5的矩阵中的右上三角元素都设置成0,其余元素值不变。
/* 例6.12源程序,5×5的矩阵中的右上三角元素设置成0的函数。 */
#include<stdio.h>
void main()
{
int a[5][5];
int i,j ;
void change(int x[ ][5],int n, int m);
printf("Matrix\n");
for(i=0; i<5; i++){
for(j=0; j<5; j++){
a[i][j]=1+i+j;
printf("%3d",a[i][j]);
}
printf("\n");
}
change(a,5,5); // 调用函数
printf("\nNew Matrix:\n");
for(i=0; i<5; i++){
for(j=0; j<5; j++)
printf("%3d",a[i][j]);
printf("\n");
}
}
void change(int x[ ][5],int n , int m)
{
int i,j;
for(i=0; i<n; i++)
for(j=0; j<m; j++)
if (i<j) x[i][j]=0;
}
程序执行:
Matrix:
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
4 5 6 7 8
5 6 7 8 9
New Matrix:
1 0 0 0 0
2 3 0 0 0
3 4 5 0 0
4 5 6 7 0
5 6 7 8 9
程序说明:change函数是二维数组作为函数的参数。函数change中定义的形参int x[ ][5],表示形参x是一个具有5列的二维数组。函数调用时,实参a是数组名,即把数组a的地址传给形参数组x,形参数组x共享实参数组a的存储区域,即函数中对形参数组x的操作,实际就是对实参数组a的操作。而形参n,m是控制对数组处理时的行数与列数,使函数具有灵活性。如,若将上例函数调用语句“change(a,5,5);”改为“change(a,3,3);”则程序运行后,“修改后输出的矩阵:”如下:
New Matrix:
1 0 0 4 5
2 3 0 5 6
3 4 5 6 7
4 5 6 7 8
5 6 7 8 9
这里虽然形参数组x与实参数a是同一个5行5列的二维数组,但形参n、m分别为3、3,即指定函数中是对数组x的前3行前3列的子矩阵进行处理,所以返回主函数后,从输出结果可以看到数组a第4、5行、第4、5列数据的未被修改。
编程提醒:在定义二维形参数组时,第一维的长度即行数是不起作用的,所以一般缺省,但第二维长度必须明确指明,并在函数调用时,与实参数组的第二维长度完全一致。若上例change函数的首行改为“void change(int x[ ][4],int n,int m)”,则函数调用时,无论用“change(a,5,5);”或“change(a,3,3);”都将导致错误的结果,因为实参数组a的第二维长度是5与形数组x的第二维长度4不一致。
有关传地址,在第8章指针中进一步介绍。
*6.3.4 参数求值顺序
当一个函数带有多个参数时,C语言没有规定函数调用时,对实参的求值顺序,不同的编译系统对此可能做不同的处理。常见的实参求值顺序是从右至左进行。
例6.13 实参的求值顺序。
/* 例6.13源程序,实参的求值顺序。 */
#include <stdio.h>
void main()
{
int n, s;
int add(int x,int y);
n=5;
s=add(n,++n);
printf("s=%d\n",s);
}
int add(int x,int y)
{
return (x+y);
}
程序执行:
s=12
程序说明:调用“add(n,++n);”的返回值12。系统采用的是从右到左的顺序计算实参,即先求实参“++n”的值,“++n”前缀格式先自加,因此复制到形参变量y中的值是6,即自加后的n值;然后将第一个实参n的值(此时为6),复制到形参变量x中。
由于参数求值顺序具有不确定性,取决于具体的编译系统。对例6.13若编译系统参数求值顺序不是从右到左,而是从左到右,则调用add后的返回值是11。
建议在程序中尽量避免由于参数求值顺序等因素所导致的结果不确定(依赖于编译系统)。可以对例6.13做适当修改,把主函数中执行语句改为“n=5;k=++n;t=n;s=add(t,k);”,则函数调用时与编译系统的参数求值顺序无关。