c语言基础教程 - 第五章函数和存储类免费版

更新时间:2024-05-04 10:24:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

完全免费版

第五章 函数和存储类

5.1.1 函数的定义

如果想调用一个函数完成某种功能,必须先按其功能来定义该函数。函数定义的格式如下所示:

<存储类说明><数据类型说明><函数名>(<参数表>) <参数说明> { <函数体> }

函数的定义可分两大部分:函数头和函数体。函数头包含(函数名>和(参数表)以及关于该函数的(存储类说明)、《数据类型说明),还有<参数说明).函数体是由一对花括号括起来的若干语句组成的,函数体内可以有一条语句或多条语句,也可以有复合语句,还可以是空,即该函数什么操作也不做,这是一个最简单的函数。(函数名)的起法同标识符,能够通过函数名来标明该函数的主要功能为最好。(函数名>后面跟一对圆括号,内有一个<参数表、,该'参数表)由一个或多个参数构成,多个参数之间用逗号分隔,也可以没有参数,但圆括号不可省略。定义瞬数时要对该函数的存储类和数据类型进行说明。函数的存储类有两种:外部函数和内部函数。外部函数用extern关键字加在函数名前面进行说明,常常省略,凡是不加存

储类说明的函数都是外部函数。内部函数必须在函数名前面加stai}c关健字进行说明,内部函数又称静态函数。函数的数据类型是该函数返回值的类型,函数的数据类型千一分丰富,一般C语言所允许的数据类型大多都可作为函数类型,个别的除外,如联合类型等。C语言规定,数据类型中除了

int型的可以不要说明外,其余各种类型都需说明。如果一个函数具有参数时,则需对参数的类型进行说明。<参数说明)一般放在函数名的下一行,有的编译系统(如Turbo G)可以把参数说明放在参数表中。

下面是函数定义的几个例子: Nothing() { }

该函数名字是nothing,它没有参数,它的函数体是空,该函数什么也不做。空函数可以用于调试中,它表明应该调用一个函数,但该函数尚未编好,先用一个空函数顶替。 void nopa() {

printf (\); }

该函数名是nops,该函数没有参数,因此不必进行参数说明,这

里,void关键字用来说明该函数没有返回值。该函数的函数体内仅有一个语句。 float max(x, y) float x, y; { float z z一x>y? x:y: return(z);

该函数名是max,它有两个参数x,y,它们都是float型数,由于该函数有参数,因此需要

参数说明:float x, y;该函数的数据类型为float型,这说明该函数有返回值,而返回值的类型是float型。该函数。无存储类的说明,这意味着该函数的存储类为外部的。该函数体是由3条语句组成的。这里有说明语句,它被放在执行语句的前面。 在函数的定义中还应注意如下几个问题:

(1)C语言中的函数可分为有返回值和无返回值两大类。在有返回值的函数定义中,除int型返回值外都必须对返回值的类型进行说明。在无返回值的函数定义中,可以加上无返回值的说明符void,也可以不加。这样,对于一个不加数据类型说明的函数可能是无返回值的,也可能是有int型返回值的。

(2)函数的定义不能嵌套。这就是说,不能在一个函数的函数体内再定义一个函数。例如,下列写法是错误的: fl(x,y) int x, y; ( f2(a,b) int a,b; {

Return(a十b); }

这种企图在f1()函数中再定义一个f2()函数的做法是错误的。但是,函数的调用允许嵌套。这就是说,可以在一个函数体内调用另一个函数,也可以进行自身调用(又称递归调用).

(3)如果一个函数的函数体中有变量的定义或说明时,一定要放在执行语句的前边,不可放在中间或后面,否则要出编泽错。 5.1.2 函数的说明

函数定义好后,在调用之前一般地要进行说明。函数的说明方法有如下两种:

一是只说明函数的类型,这称为简单说明。例如,对前面定义过的函数max说明如下:

float max();

这种说明可以单独一行,也可以与其他同类型的变量放在一起。 二是不仅说明函数的类型还要说明其参数的类型,这称为原型说明。例如,对前面己定义过的函数max ( )原型说明如一F: Boat max (float,float);

原型说明要比简单说明复杂一些,但是用原型说明后,在函数调用时系统将其参数的类型进行检查,如果发现不一致,则报错。如果用简单说明方法,则在调用时不作参数类型是否一致的检查,即使不一致也不报错,由此可见,原型说明比简单说明要安全些。 在实际使用中关于函数的说明还需注意如下几点:

(1)在下列情况下,函数在调用之前不必说明:在定义函数时没有加任何说明;或者是该函数无返回值义没有加void说明符;或者是该函数具有int型返回值而省略说明符。

(2) 助在下列情况下,函数在调用之前必须说明,如不说明,将出现编译错:即在定义函数时加了说明符,包含void在内,又是先调用后定义(即调用在定义之前),则调用之前必须说明。

(3)在下列情况下,即在定义函数时加了说明符,包含void在内。而先定义后调用(即定义在调用之前),则调用函数之前可以说明也可以不说明。

有时为了使得系统能够检查函数调用时其参数类型是否一致,对于不必须说明函数的情况也对函数进、

5.2.1 函数的参数

C语言中,有的函数有参数,可以一个或多个,多个参数用逗号分隔,而有的函数没有参数,但是函数名后面的圆括号不可省略,例如,前面讲过的主函数main( )就是属于没有参数的函数。但是,主函数也可以有参数,后面会讲到,

函数参数分为形式参数(简称形参)和实在参数(简称实参)两种。形参是指在定义函数时,(参数表)中的参数,因为该参数在该函数被调用之前是没有确定值的,只是形式仁的参数,只有被调用时通过实参来获取值。实参是指调用函数的参数,因为该参数已具有确定的值,是实在的参数:实参可以是表达式,在调用之前先计算出表达式的值。

在实际应用中关于形参和实参的使用还应注意如下几点: (1)在定义函数时,所指定的形参在该函数被调用之前是不被分配内存单元的。只有在发生函数调用时,才给形参分配单元,井且

赋值,一旦函数调用结束后,形参所占的内存单元又被释放掉。因此,形参属于局部变量,其作用范围仅在定义它的}J数体内。 (2)函数调用时所用的实参是个具有确定值的表达式,调用时先计算表达式的值,再将其值传递给对应的形参。

(3)函数的形参是属于定义它的函数的局部变量。因此,允许一个函数的形参和实参同名,因为它们在内存中占有不同的存储单元。

(4)函数调用要求形参和实参在个数上相等,并且对应的参数类型相同。否则将会发生\个数不匹配\或\类型不一致\的错误。一般情况下,即使不报错误信息,也会造成结果不正确。

下面举一个简单的函数调用的例子,通过它进一步说明函数的定义格式,函数的说明方法和形参与实参的不同。 [例5. 1] 求两个float型数的和。 程序内容如下: main() {

float a,b,sum; float fadd(); a=5.6;

b=7,2; sum=fadd(a,b);

printf (\”,sum); }

float fadd(x,y) float x,y {

retutrt(x+y);

执行该程序输出如下结果: 12. 80 说明:

(1)该程序中定义了两个函数:main ( )和fadd ( ).前一个是主函数,它无参数,也无返回值。后一个是具有float型返回值的带有参数的函数,该函数有2个参数x和Y,它们都是float型变量。

(2)在主函数main)中,float fadd ( );这是对函数fadd()的简单说明,只说明函数的类型是float型(即返回值的类型).如果甩原型说明应采用如下格式: float fadd(#loat,float);

这种情况下,对fadd < )函数必须说明,因为该函数返回值非int型的,并且又是调用在先定义在后。如不说明、则会出现编译错,读者可以上机验证。

(3)该程序中,x和Y是fadd < )函数的两个形参,关于形参类型说明的方法,除了本程序中说明方法外,还可以采用如下方法: float fadd

即在参数表中就说明了参数的类型。但是,这种说明方法并不是所有编译系统都允许。而Tuiba C编译系统是允许的。

该程序中,a和b是fadd()函数的两个实参,在调用该函数时,a和b的值是确定的。

该程序中的函数调用时满足了形参与实参个数相等,对应类型相同的要求。

5.2.2 函数的返回值、

C语言中,有的函数带有返回值,有的函数不带有返回值。 函数的返回值都是通过return(表达式))语句来实现的。例如,在本章例5. 1中,fadd()函数的函数体中的return(x+y)十妇;语句就是将表达式x十Y的值作为返回值,返回给调用函数fadd ( )的值赋给变量sum o返回值的类型是该函数的类型。带有返回值的return语句具体实现过程如下:

(1)先计算return语句中《表达式)的值,

(2)根据函数的类型对哎表达式)的类型进行转换,使得(表达式》的类型转换成为函数的类型;

(3)将<表达式>的值和类型返回给调用函数,作为调用函数的值和类型。一般情况下,要设置一个变量来接收这一返回值。 (4)将程序流的控制权交给调用函数,继续执行调用函数下边的语句。

在不带返回值的,turn语句中。不执行上述(1)至(3)步较,只执行(4)步魏。

函数的返回值(简称函数值)的类型是在定义函数时指出的一般情况下,除了返回值为int型的不要说明函数的类型外,对非int型返回值的函数都应指出西数值的类璧。函数遭的类型和、turn语句中表达式的类型可能不一致,这时藉要转换,即将表达式的类型转换成为函数值的类型,这种转换是自动进行的。下面通过一个实例说明当函数值的类型与return语句表达式的类型不一致的情况时是如何自动转换的。

[例5.2求] 两个输入的浮点数的和,最后的和用int型表示。 程序内容如下: main ( )

{

foat x,y, int sum;

printf(\“); scant(\ sum=add(x,y);

printl (\} addt(x,y) float x,y. {

return (x+y); 运行情况如下: Input x,y:1,3,7,9 sum=9 说明:

(1)该程序由两个函数组成,一个是main()函数,它无参数,也无返回值;另一个是add()函数,它有两个参数x和Y}都是float型的。它有int型返回值。因此,定义该函数时可以不加类型说明,调用该函数之前也可不加说明。

(2)在main()中,x和Y作为addt)函数的实参;在add ( )函数中,x和Y作为add函数的形参。虽然名字相同,但它们在内存中有着不同的存储单元。调用add()函数时,其形参与实参是一致的,即个数相等,对应类型相伺。

(3)在被调用函数add()中,返回值的表达式x十Y的类型是float型的,其值应为i_. 3十7.9,即为9. 2 0由于函数add必的类型为int型的,因此,要将9. 2自动转换为int型数9,再返回给调用函数,即赋值给int型变量sum。

5.3.1传值谓用的特点、

这里所讨论的传值调用是指传递变量值的调用方式。在这种调用方式中,实参使用变量名或者表达式,形参使用变量名。在调用时,调用函数将实参值拷贝到一个副本给形参,即使形参按顺序从对应的实参中获得值,这就相当于将实参值对应地赋给形参,使形参获值。这种调用方式具有如下特点:被调用函数的参数值被改变而不会影响调用函数的参数值,因此安全性好。 [例5.3] 分析下列程序输出结果。 执行该程序输出结果如下: Main( ) (

float a,b;

void fl()\ a=7. 2; b=3. 6; fl(a*b);

printf ('\,2f\\n\}

void fl(x*y) float x.y; (

x+=0. 5; y一=0.5;

printf (\,%.2f\\n\,x,y); {

7.70,3.10 7.20,3.60 说明:

(1)该程序由两个函数组成,一个是主函数main ( ),另一个是被main( )函数调用的被调用函数f1().在定义fl()函数时,使用了void说明该函数无返回值。因此,在调用f1()函数之前必须说明flt)函数,因为调用在先定义在后。

(2)主函数中,调用fl ()函数时,两个实参都是变量,即a和b.被调用函数的两个形参也都是变量,即x和y,这是属传递变量值的传值调用方式。在被调用函数fl()中,通过两个赋值表达式语句分别对形参x和Y的值进行改变,并通过printf ( )语句输出改变后的x和Y的值:7. 70, 3 ,10.返回主函数后,执行printf)语句输出主函数中的a和b值为7.20, 3.60.可见,a和b值没有因为调用函数fl()而改变其值。尽管在fl()函数中对其形参x和Y进行了改变。可见,这种传值调用方式使得被调用函数中参数值的改变不影响调用函数的参数值,其原因是因为这种调用方式是调用函数将其实参值拷贝了一个副本给了被调用函数的形参,被调用函数中改变形参的值,只是改变了副本中的值,对调用函数中的\正本\没有改变。

5.3.2 传址调用的特点

传址调用是指在调用时传递变量地址值的传值调用。传址调用时要求调用函数的实参用地址值,而被调用函数的形参用指针,于是函数之间进行地址值的传递。这种传递是将实参的变量地址值传递给形参指针,即让形参指针指向实参变量,这种传递方式与调用函数拷贝实参值的副本给形参是不同的,它是让形参指针直接指向实参的变量。这种传址调用具有如下特点:被调用函数中可以通过改变形参所指向的内容来改变调用函数的实参值:这一特点与前面讲过的传值调用是截然不同的。传址调用可以通过改变形参所指向的内容来改变实

参值,这就提供了函数之间进行信息传递的又一渠道,并且这种传递信息的方式还可以克服前面讲过的返回值方式的只传一个信息的局限性。因此,传址调用在C语言函数调用中是经常采用的方式。后面还会看到这种调月方式将会带来其他好处。

下面举一个传值调用方式和传址调用方式不同的例子,并且将看到这两种传递方式可在同一个k}数调用中实现。 [例5.4] 分析下列程序输出结果。 Main() {

Int a,b,c, b=c=a=5; Fl(n,&b,&c);

printf(f'%d,%d,''d\\n\); )

Fl(x,y.z) int x*y,* z: { X*-2 y一一x; *z=x十* y:

printf (\)

执行该程序输出如下结果: 10,15,25 5,15 ,25 说明:

(1)该程序由主函数main ( )和被调用函数fl()组成,fl ()函数无返回值,由于定义fl()时没有加任何类型说明符,尽管调用在先定义在后也不必说明。

(2)在调用函数fl()时,3个实参中,一个是用变量名,另外2个用变量的地址值;在对应的形参中,一个是用变量名,另外2个用指针。在调用fl()函数中,有一个参数属于传值调用,2个参数属于传址调用。在被调用函数f1()中,改变了变量x的值,由于传值调用对调用函数对应实参a的值并没有被改变,这是传值调用的特点。同时,通过用运算符,对指针y和:取其内容,并且通过赋值表达式语句来改变了x和Y的内容,于是调用函数中对应的实参b和c的值被相应改变,这便是传址调用的特点。

该例程序中值得注意的是要在传址调用中通过被调用函数来改变调用函数的参数值时,一定要改变形参指针所指向的内容,而不能只改变形参的地址值,改变形参地址值是不会影响对应实参的变量值的。下面的例子将说明这一点。

[例5.5] 分析下列程序的输出结果。 main ( ) {

int a,b,c,d; a=b==c=d=5; f1(&a,&b); f2(&c,&d),

printf(\) } fl(x,y) int *x,*Y} { *x*=2; y+=*x;

printf(\); } f2(m,n) int *p,*q; p=q=8; p=q=8; m=&p n=&q

;

printf(\); }

执行该程序输出结果如下: 10,15 8, 8 to,15,5 ,5 说明:

(1)该程序由三个函数组成,除了主函数main()外,还有二个被调用用函数fl()和f2()。这两个被调用函数都是没有返回值的。在main ( )函数中,先调用f1()函数,再调用f2()函数。

(2)在调用fl()函数时采用了传址调用方式,即实参用变量a和b的地址值&a和&b,形参用指针x和Y.在fl()函数中,通过使用取内容运算符改变了指针x和Y所指向的变量的值(即改变了指针所指向的内容).因此,fl()函数被调用后,土函数中变量a和b的值发生了变化,这是通过传址调用改变调用函数参数值的又一个例子。

(3)在调用f2()函数时也采用了传址调用方式,即实参用变量。和d的地址值&c和&d,形参用指针m和n.在f2()函数中,形参m和n的地址值发生了变化,使m和n指向了变量P和q,于是m

和n所指向的内容将是P和9变量的值8.这时,如果要再改变m和n所指向的内容,只是改变变量P和q的值,也不会影响调用函数的参数值。因此,简单地说\传址调用中通过改变形参的值来改变调用函数中参数的值\这句话是不确切的。一定要指出在被调用函数中通过改变形参所指的内容才能改变调用函数中实参的值。

5.3.3 数组名作参数的函数调用

数组元素可作为实参,实现传值调用,这与变量名作实参一样,都是单向传递的。

数组名作函数实参与数组元素作实参是不同的,因为C语言规定数组名是一个地址值,即是该数组首元素的地址值。因此,数组名作实参时,要求形参也是数组名或者指向数组的指针,关于指向数组的指针将在下一章\指针\中讲解,这时实现的不是传值调用,而是传址调用。调用函数不是将整个数组的所有元素拷贝成副本传递给被调用函数,而是只将其数组的首元素地址值传给形参数组,于是这两个数组将共同占用同一段内存单元,这就是让形参数组的首元素地址与实参的首元素地址相同,使得这两个数组的对应元素同占一个内存单元。因此要求这两个数组要类型相同,数组的大小可以一致,也可以不一致。如果要使形参数组得到实参数组的全部元素,则形参数组与实参数组应大小一致。

下面举例说明用数组名作函数参数的调用方法。

[例5. 6] 编程对某一数组中的各个int型数进行由小到大的排序。

本例中采用算法简单的选择排序法。程序内容如下: main () {

void sort();

static int a[8]={5,一2 .9,87,0,6,21.49:}; Int i n: Sort(a.n);

for(i=O;i<8;i十十)

printf (\printf (\}

void sort(b,m) int b[],m; {

int i,j,k,temp; for(i=O;i

for(j=i+1;j

if(b[j]

执行该程序输出结果如下:

一2 0 5 6 9 21 49 87 说明:

(l)该程序是通过调用sort( )函数对已知数组a中的8个int型数进行由小到大排序。被调用函数sort()是没有返回值的。 (2)主函数中调用函数格式如下: sort(a,n)

其中,实参a是一个已知的数组名,该数组是一个一维数组,8个int型元素。实参n的值为8.可见,sort ()函数的两个实参中,一个是地址值,另一个变量值。

被调用函数sort( )的两个形参中,一个是数组名b,它是一个没有指定大小的一维int型数组,其类型与数组a相同,其大小将与所对应的实参数组大小相等。另一个形参是变量m.因此,不难看出该函数调用中,第一个参数是属于传址调用,第二个参数是传值调用。在传址调用中,将数组a的首元素地址传给b数组,使得a,b两个数组共占同一段内存单元。由于是传址调用不必拷贝数组元素的副本,因此,效率较高,节省了时间和空间的开销。这是传址调用的又一好处。

由于a,b数组共占同一段内存单元,因此在。rt()函数中,通过b数组元素的改变,而使得数组a的元素发生了变化,于是完成由小到大的排序。

(3)在函数sort( )中,选择排序法的算法是这样的:通过双重for循环,每作一次外重for循环,从指定的数中挑出最小的一个放在指定的元素位置,例如,第一次外重for循环,从5个数中挑出最小的放在第。个元素的位置,第二次外重循环从剩下的7个数中再挑出最小的放在第1个元素的位置,依次类推,外重for循环共进行7次,便将8个数的顺序由小到大排好。内重for环是用来确定在要查找的数中找出最小数所对应的下标值,并将它赋给k变量。然后,通过三个赋值语句将数值最小的那个元素换到待选数中的最前边。即下标值为i的位置。这种排序方法每次找出一个最小的数放在前边,直到最后剩下一个数为止。

5.3.4函数的嵌套调用、

所谓函数的嵌套调用是指在调用一个函数的过程中,又调用另外一个函数。例如,在调用A函数的过程中,还可以调用B函数,在调用B函数的过程中,还可以调用C函数,?当C函数调用结束后返回到B函数,当B函数调用结束后返回到A函数,当A函数调用结束后再返回到A的调用函数中。假定main( )调用A函数,上述的嵌套调用关系可用下图所示:

图中①②③一表示了执行嵌套过程的顺序号。即从①开始,经过了三级嵌套,到⑩结束。

这里还需重申一遍,C语言中函数的定义不允许嵌套,就是说不允许在函数中定义函数,C:语言程序中若干个函数都是平行的、独立的。函数之间是通过调用联系的。函数的调用是允许嵌套的,就是说在调用某个函数的过程中,还允许调用其他函数。

下面举一个函数嵌套调用的例子,通过该例搞清调用、返回之间的关系。

[例5.7」分析下列程序的输出结果。

main()

printf (\a();

printf (\} a()

printf<\b();

printf (\} b() {

printf (\c();

printf (\C()

printf(\{ c() {

printf(\”): }

请读者分析该程序后,写出输出结果。 [例5.8] 编程求出下式之和。 lk+2K+3k+??+nK

假定k为4,n为6,编写求上述和的程序如下: # define K 4 t}define N 6 main ( )

printf(\powers of integers from 1 to%d=\ printf (\一of_powers (K,N)); }

sum of一powers(k,n) int k,n; {

int i,sum=0; for(i=1 i<=n;i++) sum+=powers(i,k); return (sum ), ;

powers (m.n ) int m,n

{

int i,product=1; for(i=l;i<=n;i++) product*=m: returnCproduet); )

执行该程序输出结果如下:

sum of 4th powers of integers from 1 to 6=2275 说明:

(1)该程序由main ( ) ,sum-of-powers)和powers)三个函数组成,main ( )中调用sum-

of-powers ( )函数,该函数返回一个int型数值,而sum-of-powers ()函数中又调用powers ( )函数,该函数也返回一个int型数值。从中可见,函数之间的嵌套调用在实际编程中是经常使用的。

(2)在主函数中,调用,sum-of-powers ( )函数时,实参是两个符号常量K和N.可见符号常量与一般常量一样都可作为函数的实参。本程序中的两次函数调用都属于传值调用,硒数之间的信息传递是通过返回值来实现的。

5.3.5 函数的递归调用

c语言中允许使用函数的递归调用,这是C语言的又一特点。所谓函数的递归调用是指在调用一个函数的过程中出现直接地或间接地调用该函数自身。例如,在调用f1()函数的过程中,又出现了调用f1()函数,这称为直接调用;而在调用fl()函数过程中出现了调用f2()函数,又在调用f2()函数过程中出现了调用f1()函数,这称为间接调用。 1.递归调用的特点

实际中不是所有的间题都可以采用递归调用的方法。只有满足下列要求的问题才可使用递归调用方法来解决:

能够将原有的问题化为~一个新的问题,而新的问题的解决办法与原有问题的解决办法相同,按这一原则依次地化分下去,最终化分出来的新的问题可以解决。

实际中有意义的递归问题都是经过有限次数的化分,最终可获得解决。这是有限递归问题,而那些无限递归问题在实际中是没有意义的。下面举一个有限递归的例子。例如,求5!由于5!可以化为5*4!.而4!又可化为4*3!,而3!可化为3,2!,而2:可化为2*1!,最后,1!可化为1* 0!,而0!是已知的,即为1.于是,可知5!等于5*4*3*1*1即120.这是一个简单的典型的递归调用的例子。

使用递归调用方法编写程序简洁清晰,可读性强。因此,人们都喜欢用递归调用的方法来解决某些问题。但是,用这种方法编写的程序执行起来在时间和空间的开销上都比较大,即要占用较多的内存单元,又要花费很多的计算时间。因为递归调用时要占用内存的许多单元存放\递推\的中间结果,较复杂的递归占用内存空间较多。因此,在一些内存小速度慢的小机器上最好不要采用递归调用的办法,不然,效率很低。一般的凡是可用递归调用方法编写的程序都可以用迭代的方法来编写。一般说来,相同的间题用迭代方法编写要比用递归调用方法编写的源程序长些。 2.递归调用的过程

递归调用的过程可分为如下两个阶段:

第一阶段称为\递推\阶段:将原问题不断化为新问题,逐渐从未知的向已知的方向推测,最终达到已知的条件,即递归结束条件。 第二阶段称为\回归\阶段:该阶段是从已知条件出发。按\递推\的逆过程,逐一求值回归,最后到递推的开始之处,完成递归调用。可见,\回归\的过程是\递推\的逆过程。

下面以求5!为例写出递归调用的全过程如下所示:

从上述列举的递推过程中可以看出,有实际意义的递归应该是有限的,即递推若干次后。将出现已知条件。并且每递推一次都是向已知条件接近一步。这里的已知条件就是递推结束条件。上例中,0!=I是已知条件,即递推结束条件。在回归的过程中,是按照递推的逆过程进行的,最后得到原问题的解。

[例5.9」用递归调用方法编程求某个正整数的阶乘。 程序内容如下: main () { int n;

scant(\);

printf (\(n)); } fac(n)

int n { int p;

if(n==O) p=1; else

p=n*fac(n一1); return (p);

执行该程序,从键盘上输入一数后,输出结果如下: 5 120

说明:该程序由主函数main)和fac()函数组成,fac<)函数中采用了递归调用的形式,即在fac(n)函数中调用fac(n-1).该递归结束条件时,n等于0,P值为1,每递归一次n减1,经过若干次递归后,n会为0。

[例5.10」汉诺(Hanai)塔间题。这是一个典型的递归调用问题。该问题描述如下:在一张桌子上有A,B,C三处,在A处有n个盘子,每个盘子大小不等,大的在下小的在上。要求将A处的n个盘子移到C处,可以借助于B处,每次移动只允许动一个盘子,在移动过程中在A,B,C三处都应保持大盘在下,小盘在上。编程打印出移动的过程。

分析算法如下:

该题目使用递归调用的方法编程。将n个盘子从A处移到C处可分为如下三个步骤:

(I)将A处的n-1个盘子借助C处移到B处。 (2)把A处剩下的一个盘子移到C处。

(3)再将B处的n-1个盘子借助于A处移到C处。 这样完成了n个盘子的移动。

分析上述的三步操作可以发现,第1步和第3步所采用的方法是一样的,都是将n-1个盘子从某一处移到另一处,只是移动的位置不同。因此,可将上述的三步骤化简为如下两步骤: (1)将n一1个盘子从一处移到另一处。 (2)将一个盘子从一处移动另一处。

把这两个步操作分别用两个函数来描述。第一步操作用move_n()函数来实现,第二步操作用二ove_1()函数来实现。 move_n<)函数有4个参数,分别为n,a,b.c,表示\将n个盘子从A处借助B处移到C处\ 该函数具体定义如下:

void move_n(n,a,b,c) ant n; char a,b,c; {

if (n==1) move 1(a ,c); else {

move n (n一l,a.c,b); mnve_ 1(a,c); move_n(n一1,b,a ,c); } }

move_n()函数有2个参数,一个是from,另一个是to,表示将i个盘子从fom处处。该函数具体定义如下: int m

void move_1(from,to) char from,to: {

if (m%8==0) printf (\);

printf(\);

m++; }

该程序主函数内容如下

void move_ 1(). move_ n(), main() { int d;

printf (\); scant(\);

printf (\) move一n(d,'n',B',C.); printf (\); 执行该程序输出如下信息: Input the number of diskes:3 \ A-C A-B C-B A-C B-A A-C 再运行一次该程序:

input the number of dishes:4 'C'he sreps to moving 4 diskes:

;

A-B A-C B-C A-B C-A C-B A-B A-C B-C B-A C-A B-C A-B A-C B-C 再运行一次该程序:

Input the number of diskes:5 The steps to moving 5 diskes: A-C A-B C-B A-C B-A B-C A-C A-B C-B C-A B-A C-B A-C A-B G-B A-C B-A B-C A-G B-A C-B C-A B-A B-G A-C A-B C-B A-C B-A B-C A-C 说明:

(l.)该程序由3个函数组成:main(),MOVE_1()和move_ n ( ).主函数main ()中调用move_n()函数,在xnove_n()函数中又调用move_1( )函数,由于move_ 1( )函数和move_n()函数在定义时使用了void来说明它们是无返回值的,因此,如果main ( )在前,move _ n ( )和move _ 1( )函数在后,则调用后两个函数之前要说明,该程序将说明放在文件头,即主函数的前面。 (2) move_n()函数采用了递归调用方法,即在move_n(n,\)函数体中两次调用move _ n(N一1,一)函数。该问题采用递归调用方法编程是很简洁明了的。用其他编程将是十分复杂的。

(3)本例中三次运行这个程序,给出了输入分别为3个盘子、4个盘和5个盘子的三组输出数据,表明了三种移动的过程。

5.4.1 标识符的作用城规则 标识符的作用域规则描述如下:

标识符只能在说明它或定义它的函数体或分程序内是可见的,而在该函数体或分程序外则是不可见的。 现将这段描述说明如下:

(1)大多数的标识符对它说明或对它定义是一回事,只有少数的标识符对它说明或对它定义是两回事,例如,外部变量和函数。 (2)标识符包含了变量、函数、语句标号、符号常量等名字,凡是用标识符规则定义的各种单词都属于标识符。

(3)可见的指的是可以进行存取、访间,可见时对它的改变是有效的,不可见的是指不能对它进行存取、访间及其他操作。可见的与存在的是两个不同概念:可见的是指在其作用域内,对它可以操作;存在的是指其寿命,仍存放在内存内,没有被释放。可见的一定是存在的,不存在一定不可见,但是存在的不一定都可见,有的标识符它虽然存在,但不可见。后面在存储类中会详细讲到,内部静态变量有时不可见了,但它仍然存在。

各种标识符的作用域是不相同的,有的是程序级,有的是文件级,还有函数级和程序段级。下面列举出一些标识符的不同作用域级别。 函数有程序级的,如外部函数;有文件级的,如内部函数。 变量中,外部变l孜属于程序级,外部静态变墩属于文件级,函数形参属于函数级。还有定义或说明在函数内的自动变量和内部静态变量以及寄存器变晕都属于函数级的,定义在分程序内的自动的、内部静态的和寄存器的变量属于程序段级的语句标号属于函数级的。 语句标号属于函数级的。 符号常量属于文件级的。

这里所说的程序级的是指作用范围(即作用域)在整个程序内,包含该程序的所用文件。同样级的是指作用范围在定义它的文件中,往往是从定义时开始。函数级的是指作用范围在定义它的函数体内;程序段级的是指作用域在定义它的分程序内。

5.4.2 重新定义变量作用域规定

C语言中,一般说来变量不能重复定义,这是指在相同的作用域内,不能有同名变量存在。

但是。在不同的作用域内,允许对某个变量进行重新定义。例如,在某个函数体内定义了一个int型变量a,这时不可以在同一个函数体内再定义一个float型变量ao但是,可以在该函数体内的某个分

程序中,对变量a重新定义,于是在整个函数体内的不同分程序中将出现同名变量。这时,它们的作用域又是如何规定的呢?关于重新定义的变量的作用域规定如下。

在函数体或分程序内可以各自定义变量,在函数体内又允许嵌套分程序。在函数体或外层分程序中定义的变量。如果在内层分程序中没有重新定义,则在内层分程序中将仍s}右效;如果在内层分程序巾进行了重新定义,则外层中的该变敏被隐藏起来,而在内层中起作用的是内层重新定义的变量,当退出内层程序又回到外层分程序或函数体内时,外层定_义的该变量又恢复出来,仍然起作用。 下面通过一个程序例子来理解和体会上述规定的含义, [例5.11〕分析下列程序的输出结果。 main ( ) {

int a=2,b=4,c=6

printf(\); { int b=8: float c=8. 8;

printf(\); a=b;

{ Int c; c=b:

printf(\); }

printf(\); }

printf(\) 执行该程序输出结果如下: 2,4,6 2,8,8. 8 8,8,8 8,8,8. 8 8,4.6

说明:该程序仅有'个主函数main( ),该函数中定义了a, b,.三个int型变量,并斌了初值。函数体内包含了一个分程序。该分程序内,重新定义了变量b和c,并赋了初值,这时函数体内定义的b和。在这里被隐藏起来,而重新定义的b和c在起作用,这时输出的a值是原来的,b和c是重新定义的。该分程序内,对a重新赋值,这不是重新定义,只是改变了a的值,因为a在这里是可见的。然后,在该分程序中,又定义了一个分程序,称内层分程序。在内层分程序

中,重新定义了。,并给它赋了值,在这里,开始定义的a仍然可见,开始定义的b仍被隐藏,外层分程序中重新定义的b是可见的,这里定义的c在起作用,外层分程序定义的。也被隐藏起来。当退出内层分程序后,开始定义的a和外层分程序定义的b和。是可见的。这时在内层分程序中,被隐藏的b和c被恢复了。当退出外层分程序后,开始定义的a和b,c都可见的。而被重新定义的b和c不可见了。值得注意的是变量a在整个函数体的任何部分都是可见的,因此,不论在何处它的值被改变后,则仍然保留其改变后的值。

5.5.1 变量的存储

C语言规定变量的存储类分为如下4种: (1)自动存储类变量(auto); (2)寄存器存储类变量(register); (3)外部类变量(extern); (4)静态类变量(static).

下面从两个方面来讨论不同存储类的特征,一是作用域和寿命,二是初始化。

1.不同存储类变蚤的作用域和寿命

(1)自动类变量的作用域是在定义它的函数体或分程序内,一旦退出了该函数体或分程序,则是不可见的。这类变量的寿命是短的,

它被存放在内存的动态存储区内。每次进人定义它的分程序或函数体内被动态分配存储区域,一旦退出该分程序或函数体后,所占用的内存区域被释放掉,即不存在了。

自动类变量一定出现在函数体或分程序内,自动类变量的存储类说明符是auto.多数情况下该说明符被省略。前面讲过的程序中凡在函数体或分程序内出现的没有加存储类说明符的都是自动类的。 总之,自动类的特点是作用域小、寿命短,可见性和存在性是一致的口即可见时即存在,一旦不可见了,也就不存在了。

(2)寄存器类变量的作用域和寿命与自动类变量相同,即作用域是在定义它的函数体或分程序内,寿命是短的。这类变量与自动类变量的区别在于寄存器类变量有可能被存放在GPU的通用寄存器中。如果这类变量数据被存放到通用寄存器中,则将大大提高对数据的存取速度。到底所定义的寄存器类变量能否被存放到通用寄存器中,这取决于当时CPU是否有空闲的通用寄存器。如果没有被存放到通用寄存器中,则按自动类变量处理。在一个程序中,定义寄存器类变量时,应注意如下几点:

①定义的寄存器变量的个数不能太多,因为空闲的通用寄存器数目是很有限的。

②由于通用寄存器的数据长度的限制,一般定义为寄存器变量的数据类型为char型或int型。数据长度太大的数据通用寄存器放不

本文来源:https://www.bwwdw.com/article/bihg.html

Top