C语言程序设计谭浩强版 十 二



8.1 函数概述



在C语言中可从不同的角度对函数分类。

1. 从函数定义的角度看,函数可分为库函数和用户定义函数两种。

2. C语言的函数兼有其它语言中的函数和过程两种功能,从这个角度看,

    又可把函数分为有返回值函数和无返回值函数两种

3. 从主调函数和被调函数之间数据传送的角度看又可分为无参函数和有参函数两种。


8.2 函数定义的一般形式



1. 无参函数的定义形式:


类型标识符 函数名()

{

    声明部分

    语句

}



2. 有参函数定义的一般形式:


类型标识符 函数名(形式参数表列)

{

    声明部分

    语句

}


例8.1

<span style="font-size:18px;">#include "stdio.h"

int max(int a,int b)
{
	if(a>b)
		return a;
	else
		return b;
}

int main(void)
{
	int x,y,z;
	printf("input two numbers:\n");
	scanf("%d%d",&x,&y);
	z=max(x,y);
	printf("maxmum = %d\n",z);
}</span>


8.3 函数的参数和函数的值




8.3.1 形式参数和实际参数


函数的形参和实参具有以下特点:

1.、形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存

      单元。因此,形参只有在函数内部有效。函数调用结束返回主调函数后则不能再使

      用该形参变量。

2、实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数

      调用时,它们都必须具有确定的值,以便把这些值传送给形参。因此应预先用赋值,

      输入等办法使实参获得确定值。

3、实参和形参在数量上,类型上,顺序上应严格一致,否则会发生类型不匹配”的错误。

4、函数调用中发生的数据传送是单向的。即只能把实参的值传送给形参,而不能把形参

      的值反向地传送给实参。 因此在函数调用过程中,形参的值发生改变,而实参中的值

      不会变化。


例8.2

<span style="font-size:18px;">#include <stdio.h>
int main(void)
{
	int n;
	printf("input number\n");
	scanf("%d",&n);
	int s(int n);//声明一下函数,如果把调用的函数放在前面就不用声明了
	s(n);
	printf("n = %d\n",n);
}
int s(int n)
{
	int i;
	for(i=n-1;i>=1;i--)
		n=n+i;
	printf("n=%d\n",n);
        return n;
}</span>


本程序中定义了一个函数s,该函数的功能是求Σni的值。在主函数中输入n值,

并作为实参,在调用时传送给s 函数的形参量n( 注意,本例的形参变量和实参

变量的标识符都为n,但这是两个不同的量,各自的作用域不同)。在主函数中

用printf 语句输出一次n值,这个n值是实参n的值。在函数s中也用printf 语句输

出了一次n值,这个n值是形参最后取得的n值0。从运行情况看,输入n值为100。

即实参n的值为100。把此值传给函数s时,形参n的初值也为100,在执行函数

过程中,形参n的值变为5050。返回主函数之后,输出实参n的值仍为100。

可见实参的值不随形参的变化而变化。


8.3.2 函数的返回值


函数的值是指函数被调用之后,执行函数体中的程序段所取得的并返回给主调函数的值。


1)、 函数的值只能通过return语句返回主调函数。

        return 语句的一般形式为:

        return 表达式;

        或者为: return (表达式);

2) 、函数值的类型和函数定义中函数的类型应保持一致。如果两者不一致,则以函数类

        型为准,自动进行类型转换。

3)、 如函数值为整型,在函数定义时可以省去类型说明。

4) 、不返回函数值的函数,可以明确定义为“空类型”,类型说明符为“void”。


8.4 函数的调用



8.4.1 函数调用的一般形式


C语言中,函数调用的一般形式为:

函数名(实际参数表)

对无参函数调用时则无实际参数表。实际参数表中的参数可以是常数,

变量或其它构造类型数据及表达式。各实参之间用逗号分隔。


8.4.2 函数调用的方式


在C语言中,可以用以下几种方式调用函数:

1、函数表达式:函数作为表达式中的一项出现在表达式中,以函数返回值参与表达式的运算。

      这种方式要求函数是有返回值的。例如:z=max(x,y)是一个赋值表达式,把max的返回值

      赋予变量z。

2、函数语句:函数调用的一般形式加上分号即构成函数语句。

      例如:printf ("%d",a);scanf ("%d",&b); 都是以函数语句的方式调用函数。

3、函数实参:函数作为另一个函数调用的实际参数出现。这种情况是把该函数的返回值作为

      实参进行传送,因此要求该函数必须是有返回值的。

      例如: printf("%d",max(x,y));

      即是把max调用的返回值又作为printf函数的实参来使用的。

      在函数调用中还应该注意的一个问题是求值顺序的问题。所谓求值顺序是指对实参表中

      各量是自左至右使用呢,还是自右至左使用。对此,各系统的规定不一定相同。


例8.3

#include<stdio.h>
int main(void)
{
	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


8.4.3 被调用函数的声明和函数原型

其一般形式为: 类型说明符 被调函数名(类型 形参,类型 形参…);

              或为: 类型说明符 被调函数名(类型,类型…);

C语言中又规定在以下几种情况时可以省去主调函数中对被调函数的函数说明。

1)、 如果被调函数的返回值是整型或字符型时,可以不对被调函数作说明,而直接调用。

        这时系统将自动对被调函数返回值按整型处理。
      
        例8.2的主函数中未对函数s作说明而直接调用即属此种情形。

2) 、当被调函数的函数定义出现在主调函数之前时,在主调函数中也可以不对被调函数  

        再作说明而直接调用。例如例8.1中,函数max的定义放在main 函数之前,因此可

        在main函数中省去对max函数的函数说明int max(int a,int b)。

3)、 如在所有函数定义之前,在函数外预先说明了各个函数的类型,则在以后的各主调函

        数中,可不再对被调函数作说明。


8.5 函数的嵌套调用



例子8.4   

       本题可编写两个函数,一个是用来计算平方值的函数f1,另一个是用来计算阶乘值

的函 数f2。 主函数先调f1计算出平方值,再在f1中以平方值为实参,调用 f2计算其阶乘

值,然后 返回f1, 再返回主函数,在循环程序中计算累加和。

#include<stdio.h>
/* 
本题可编写两个函数,一个是用来计算平方值的函数f1,
另一个是用来计算阶乘值的函数f2。主函数先调f1计算出平方值,
再在f1中以平方值为实参,调用 f2计算其阶乘值,然后返回f1,
再返回主函数,在循环程序中计算累加和。
*/
long f1(int p)
{
	int k;
	long r;
	long f2(int);//函数声明
	k = p*p;//主函数中调用中p分别为2和3
	r = f2(k);//把k带入到函数f2中,分别计算4和9的阶乘
	return r;
}
long f2(int q)
{
	long c = 1;
	int i;
	for(i = 1;i<=q;i++)
    {
		c=c*i;
    }
	return c;
}
int main(void)
{
	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返回主函数实现累加。



8.6 函数的递归调用


一个函数在它的函数体内调用它自身称为递归调用。这种函数称 为递归函数。

例8.5  用递归法计算n的阶乘

#include<stdio.h>
long ff(int n)
{
	long f=0;
	if(n<0)
	{
		printf("n<0,input error");
	}
	else if(n==0||n==1)
	{
		f=1;
	}
	else 
	{
		f = ff(n-1)*n;
	}
	return (f);
}
int main(void)
{
	int n;
	long y;
	printf("\ninput a inteager number:\n");
	scanf("%d",&n);
	y=ff(n);
	printf("%d!=%ld\n",n,y);
}

程序中给出的函数ff是一个递归函数。主函数调用ff 后即进入函数ff执行,如果n<0,

n==0或n=1时都将结束函数的执行,否则就递归调用ff函数自身。由于每次递归调

用的实参为n-1,即把n-1的值赋予形参n,最后当n-1的值为1时再作递归调用,形参

n的值也为1,将使递归终止。然后可逐层退回。

下面我们再举例说明该过程。设执行本程序时输入为5,即求5!。在主函数中的调用

语句即为y=ff(5),进入ff函数后,由于n=5,不等于0或1,故应执行f=ff(n-1)*n,即

f=ff(5-1)*5。 该语句对ff作递归调用即ff(4)。进行四次递归调用后,ff函数形参取得的

值变为1,故不 再继续递归调用而开始逐层返回主调函数。ff(1)的函数返回值为1,

ff(2)的返回值为1*2=2, ff(3)的返回值为2*3=6,ff(4)的返回值为6*4=24,最后返回值

ff(5)为24*5=120。

8.7 数组作为函数参数




数组可以作为函数的参数使用,进行数据传送。数组用作函数参数有两种形式,一种

是把数组元素(下标变量)作为实参使用;另一种是把数组名作为函数的形参和实参使用。

1. 数组元素作函数实参

数组元素就是下标变量,它与普通变量并无区别。 因此它作为函数实参使用与普通变量

是完全相同的,在发生函数调用时,把作为实参的数组元素的值传送给形参,实现单向

的值传送

例8.7   判别一个整数数组中各元素的值,若大于0 则输出该值,若小于等于0则输出0值。

#include"stdio.h"
void nzp(int v)
{
	if(v>0)
		printf("%d",v);
	else
		printf("%d",0);
}
int main(void)
{
	int a[5],i;
	printf("input 5 numbers\n");
	for(i=0;i<5;i++)
	{
		scanf("%d",&a[i]);
		nzp(a[i]);
	}
}

本程序中首先定义一个无返回值函数nzp,并说明其形参v为整型变量。在函数体中根据v

值输出相应的结果。在main函数中用一个for语句输入数组各元素,每输入一个就以该元

素作实参调用一次nzp函数,即把a[i]的值传送给形参v,供nzp函数使用。

2. 数组名作为函数参数

数组名作函数参数时所进行的传送只是地址的传送,也就是说把实参数组的首地址赋予形

参数组名。形参数组名取得该首地址之后,也就等于有了实在的数组。实际上是形参数组

和实参数组为同一数组,共同拥有一段内存空间。



上图说明了这种情形。图中设a为实参数组,类型为整型。a占有以2000为首地址的一

块内 存区。b为形参数组名。当发生函数调用时,进行地址传送,把实参数组a的首地址

传送给 形参数组名b,于是b也取得该地址2000。于是a,b两数组共同占有以2000为首

地址的一段 连续内存单元。从图中还可以看出a和b下标相同的元素实际上也占相同的两

个内存单元(整 型数组每个元素占二字节)。例如a[0]和b[0]都占用2000和2001单元,当

然a[0]等于b[0]。类 推则有a[i]等于b[i]。

例8.8   数组a中存放了一个学生5门课程的成绩,求平均成绩。

#include"stdio.h"
float aver(float a[5])
{
	int i;
	float av,s=a[0];
	for(i=1;i<5;i++)
		s=s+a[i];
	av=s/5;
	return av;
}
int main(void)
{
	float sco[5],av;
	int i;
	printf("\ninput 5 scores:\n");
	for(i=0;i<5;i++)
		scanf("%f",&sco[i]);
	av=aver(sco);
    printf("average score is %5.2f",av);
}

本程序首先定义了一个实型函数aver,有一个形参为实型数组a,长度为5。在函数aver中,

把各元素值相加求出平均值,返回给主函数。主函数main 中首先完成数组sco的输入,

后以sco作为实参调用aver函数,函数返回值送av,最后输出av值。 从运行情况可

以看出, 程序实现了所要求的功能。

例8.9

#include<stdio.h>
void nzp(int a[5])
{
	int i;
	printf("\nvalues of array a are:\n");
	for(i=0;i<5;i++)
	{
		if(a[i]<0) a[i]=0;
		printf("%d",a[i]);
	}
}

int main(void)
{
	int b[5],i;
	printf("\ninput 5 numbers:\n");
	for(i=0;i<5;i++)
		scanf("%d",&b[i]);
	printf("initial values of array b are:\n");
	for(i=0;i<5;i++)
		printf("%d",b[i]);
	nzp(b);
	printf("\nlast values of array b are:\n");
	for(i=0;i<5;i++)
		printf("%d\n",b[i]);
}

本程序中函数nzp的形参为整数组a,长度为5。主函数中实参数组b也为整型,长度

也为5。在主函数中首先输入数组b的值,然后输出数组b的初始值。然后以数组名b

为实参调用nzp函数。在nzp中,按要求把负值单元清0,并输出形参数组a的值。 返

回主函数之后,再次输出数组b的值。从运行结果可以看出,数组b的初值和终值是不

同的,数组b的终值和数组a是相同的。这说明实参形参为同一数组,它们的值同时得

以改变。


8.8 局部变量和全局变量


8.8.1 局部变量

局部变量也称为内部变量。局部变量是在函数内作定义说明的。其作用域仅限于函数

内, 离开该函数后再使用这种变量是非法的。

关于局部变量的作用域还要说明以下几点:

1) 主函数中定义的变量也只能在主函数中使用,不能在其它函数中使用。

2) 形参变量是属于被调函数的局部变量,实参变量是属于主调函数的局部变量。

3) 允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,

   互不干扰,也不会发生混淆。

4) 在复合语句中也可定义变量,其作用域只在复合语句范围内。



8.8.2 全局变量

全局变量也称为外部变量,它是在函数外部定义的变量。它不属于哪一个函数,它属

于一个源程序文件。其作用域是整个源程序。在函数中使用全局变量,一般应作全局

变量说明。 只有在函数内经过说明的全局变量才能使用。全局变量的说明符为extern。

但在一个函数之前定义的全局变量,在该函数内使用可不再加以说明。

例8.13  输入正方体的长宽高l,w,h。求体积及三个面x*y,x*z,y*z的面积。

#include<stdio.h>
int s1,s2,s3;
int vs(int a,int b,int c)
{
	int v;
	v=a*b*c;
	s1=a*b;
	s2=b*c;
	s3=a*c;
	return v;
}
int main(void)
{
	int v,l,w,h;
    printf("\ninput length,width,height\n");
	scanf("%d%d%d",&l,&w,&h);
	v=vs(l,w,h);
	printf("\nv=%d,s1=%d,s2=%d,s3=%d\n",v,s1,s2,s3);
}



如果同一个源文件中,外部变量与局部变量同名,则在局部变量的作用范围内,外部

变量被“屏蔽”,即它不起作用。



8.9 变量的存储类别

8.9.1 动态存储方式与静态动态存储方式

从变量的作用域(即从空间)角度来分,可以分为全局变量和局部变量。

从变量值存在的作用时间(即生存期)角度来分,可以分为静态存储方式和动态存储方式。

静态存储方式:是指在程序运行期间分配固定的存储空间的方式。

动态存储方式:是在程序运行期间根据需要进行动态的分配存储空间的方式。

用户存储空间可以分为三个部分:

1) 程序区;

2) 静态存储区;

3) 动态存储区;

全局变量全部存放在静态存储区,在程序开始执行时给全局变量分配存储区,程序行完毕

就释放。在程序执行过程中它们占据固定的存储单元,而不动态地进行分配和释放;

动态存储区存放以下数据:

1) 函数形式参数;

2) 自动变量(未加static声明的局部变量);

3) 函数调用实的现场保护和返回地址;

对以上这些数据,在函数开始调用时分配动态存储空间,函数结束时释放这些空间。 

在c语言中,每个变量和函数有两个属性:数据类型和数据的存储类别。

8.9.2 auto变量

函数中的局部变量,如不专门声明为static存储类别,都是动态地分配存储空间的,

数据存储在动态存储区中。函数中的形参和在函数中定义的变量(包括在复合语句

中定义的变量),都属此类,在调用该函数时系统会给它们分配存储空间,在函数

调用结束时就自动释放这些存储空间。这类局部变量称为自动变量。自动变量用关

键字auto作存储类别的声明。

8.9.3 用static声明局部变量

有时希望函数中的局部变量的值在函数调用结束后不消失而保留原值,这时就应该

指定局部变量为“静态局部变量”,用关键字static进行声明。

例8.15  考察静态局部变量的值。

#include<stdio.h>
int f(int a)
{
	auto int b=0;
	static int c=3;
	b=b+1;
	c=c+1;
	return(a+b+c);
}
int main(void)
{
	int a=2,i;
	for(i=0;i<3;i++)
		printf("%d",f(a));
}

对静态局部变量的说明:
1) 静态局部变量属于静态存储类别,在静态存储区内分配存储单元。在程序整

个运行期间都不释放。而自动变量(即动态局部变量)属于动态存储类别,占

动态存储空间,函数调用结束后即释放。

2) 静态局部变量在编译时赋初值,即只赋初值一次;而对自动变量赋初值是在

函数调用时进行,每调用一次函数重新给一次初值,相当于执行一次赋值语句。

3) 如果在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动

赋初值0(对数值型变量)或空字符(对字符变量)。而对自动变量来说,如果

不赋初值则它的值是一个不确定的值。

8.9.4 register变量

为了提高效率,C语言允许将局部变量得值放在CPU中的寄存器中,这种变量叫

“寄存器变量”,用关键字register作声明。

8.9.5 用extern声明外部变量

外部变量(即全局变量)是在函数的外部定义的,它的作用域为从变量定义处开始,

到本程序文件的末尾。如果外部变量不在文件的开头定义,其有效的作用范围只限

于定义处到文件终了。如果在定义点之前的函数想引用该外部变量,则应该在引用

之前用关键字extern对该变量作“外部变量声明”。表示该变量是一个已经定义的

外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。

例8.18  用extern声明外部变量,扩展程序文件中的作用域。

#include<stdio.h>
int max(int x,int y)
{
	int z;
	z=x>y?x:y;
	return(z);
}
int main(void)
{
	extern int A,B;
	printf("%d\n",max(A,B));
}
int A=13,B=-8;

说明:在本程序文件的最后1行定义了外部变量A,B,但由于外部变量定义

的位置在函数main之后,因此本来在main函数中不能引用外部变量A,B。

现在我们在main函数中用extern对A和B进行“外部变量声明”,就可以从

“声明”处起,合法地使用该外部变量A和B。


猜你喜欢

转载自blog.csdn.net/thebestleo/article/details/52145878