8.1 概述
8.2 函数定义的一般形式
8.3 函数参数和函数的值
8.4 函数的调用
8.5 函数的嵌套调用
8.6 函数的递归调用
8.7 数组作为函数参数
8.8 局部变量和全局变量
8.9 变量的存储类别
8.10 内部函数和外部函数
8.11 如何运行一个多文件的程序习题第 8章 函 数
8.1 概述一个较大的程序一般应分为若干个程序模块,每一个模块用来实现一个特定的功能。所有的高级语言中都有子程序这个概念,用子程序实现模块的功能。在C语言中,子程序的作用是由函数完成的。一个C程序可由一个主函数和若干个函数构成。由主函数调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多个函数调用任意多次。图 8.1是一个程序中函数调用的示意图。
在程序设计中,常将一些常用的功能模块编写成函数,放在函数库中供公共选用。要善于利用函数,
以减少重复编写程序段的工作量。
先举一个简单的函数调用的例子。例 8.1
main()
{ printstar();/ * 调用 printstar函数 */
print-message();/ * 调用 print message */
printstar(); / * 调用 printstar函数 */
}
printstar() / *printstar函数 */
{
printf(" * * * * * * * * * * * * * * * * * *\ n");
}
print-message()
/ * print-message函数 */
{
printf( "How do you do!\ n");
}
运行情况如下:
* * * * * * * * * * * * * * * * * *
How do you do!
* * * * * * * * * * * * * * * * * *
图 8.1
printstar和 print-message都是用户定义的函数名,分别用来输出一排,*”号和一行信息。
说明:
(1) 一个源程序文件由一个或多个函数组成。一个源程序文件是一个编译单位,即以源程序为单位进行编译,而不是以函数为单位进行编译。
(2) 一个C程序由一个或多个源程序文件组成。对较大的程序,一般不希望全放在一个文件中,而将函数和其他内容(如预定义)分别放在若干个源文件中,再由若干源文件组成一个 C程序。这样可以分别编写、分别编译,提高调度效率。一个源文件可以为多个 C程序公用。
(3) C程序的执行从 main函数开始,调用其他函数后流程回到 main函数,在 main函数中结束整个程序的运行。 main函数是系统定义的。
(4) 所有函数都是平行的,即在定义函数时是互相独立的,一个函数并不从属于另一函数,即函数不能嵌套定义(这是和 PASCAL不同的),函数间可以互相调用,但不能调用 main函数。
(5) 从用户使用的角度看,函数有两种:
① 标准函数,即库函数。这是由系统提供的,用户不必自己定义这些函数,可以直接使用它们。应该说明,不同的 C系统提供的库函数的数量和功能不同,
当然有一些基本的函数是共同的。
② 用户自己定义的函数。用以解决用户的专门需要。
(6) 从函数的形式看,函数分两类:
① 无参函数。如例 8.1中的 printstar和 print-message就是无参函数。在调用无参函数时,主调函数并不将数据传送给被调用函数,一般用来执行指定的一组操作
(例如,例 8.1那样),printstar函数的作用是输出 18
个星号。无参函数可以带回或不带回函数值,但一般以不带回函数值的居多。
② 有参函数。在调用函数时,在主调函数和被调用函数之间有数据传递。也就是说,主调函数可以将数据传给被调用函数使用,被调用函数中的数据也可以带回来供主调函数使用。
类型标识符 函数名()
{声明部分语句
}
例 8.1中的 printstar和 print-message函数都是无参函数。用,类型标识符,指定函数值的类型,即函数带回来的值的类型。无参函数一般不需要带回函数值,因此可以不写类型标识符,
例 8.1就如此。
8.2 函数定义的一般形式
1,无参函数的定义形式类型标识符函数名(形式参数表列)
{声明部分语句
}
例如:
int max( int x,int y)
{ int z;/ *
z= x> y? x∶ y;
return(z);
}
2,有参函数定义的一般形式这是一个求 x和 y二者中大者的函数,笫 1行第一个关键字 int表示函数值是整型的。 max为函数名。括号中有两个形式参数 x和 y,它们都是整型的。在调用此函数时,主调函数把实际参数的值传递给被调用函数中的形式参数 x和 y。花括弧内是函数体,它包括声明部分和语句部分。在声明部分定义所用的变量,此外对将要调用的函数作声明
(见 8.4.3节)。在函数体的语句中求出z的值
(为 x与 y中大者),return(z)的作用是将z的值作为函数值带回到主调函数中。 return后面的括弧中的值 (z)作为函数带回的值(或称函数返回值)。
在函数定义时已指定 max函数为整型,在函数体中定义z为整型,二者是一致的,将z作为函数 max
的值带回调用函数(见例 8.2)。
如果在定义函数时不指定函数类型,系统会隐含指定函数类型为 int型。因此上面定义的 max函数左端的
int可以省写。
3,可以有“空函数”
它的形式为类型说明符函数名( )
{ }
例如:
dummy(){}
调用此函数时,什么工作也不做,没有任何实际作用。
在主调函数中写上,dummy();” 表明,这里要调用一个函数”,而现在这个函数没有起作用,
等以后扩充函数功能时补充上。 在程序设计中往往根据需要确定若干模块,分别由一些函数来实现。 而在第一阶段只设计最基本的模块,其他一些次要功能或锦上添花的功能则在以后需要时陆续补上。在编写程序的开始阶段,可以在将来准备扩充功能的地方写上一个空函数(函数名取将来采用的实际函数名(如用 merge()、
matproduct(),oncatenate(),shell()等,
分别代表合并、矩阵相乘、字符串连接、希尔法排序等),只是这些函数未编好,先占一个位置,
以后用一个编好的函数代替它。这样做,程序的结构清楚,
可读性好,以后扩充新功能方便,对程序结构影响不大。空函数在程序设计中常常是有用的。
4,对形参的声明的传统方式在老版本 C语言中,对形参类型的声明是放在函数定义的笫 2行,也就是不在笫 1行的括号内指定形参的类型,而在括号外单独指定,例如上面定义的 max函数可以写成以下形式:
int max( x,y) /* 指定形参 x,y */
int x,y; /* 对形参指定类型 */
{ int z;
z = x > y? x,y;
return(z);
}
一般把这种方法称为传统的对形参的声明方式,而把前面介绍过的方法称为现代的方式。 Turbo C和目前使用的多数 C版本对这两种方法都允许使用,
两种用法等价,A NSI新标准推荐前一种方法,即现代方式。它与 PASCAL语言中所用的方法是类似的。本书中的程序采用新标准推荐的现代方式。
但由于有些过去写的书籍和程序使用传统方式,
因此读者应对它有所了解,以便能方便地阅读它们。
8.3.1 形式参数和实际参数在调用函数时,大多数情况下,主调函数和被调用函数之间有数据传递关系。这就是前面提到的有参函数。前面已提到:在定义函数时函数名后面括弧中的变量名称为“形式参数”(简称“形参”),在主调函数中调用一个函数时,函数名后面括弧中的参数 (可以是一个表达式 )称为“实际参数”(简称“实参”)。
例 8.2调用函数时的数据传递。
main()
{ int a,b,c;
8.3 函数参数和函数的值
scanf("% d,% d",& a,& b);
c= max( a,b);
printf("M ax is% d",c);
}
max( int x,int y max /
{
int z;
z= x> y? x∶ y;
return(z);
}
运行情况如下:
7,8
M ax is 8
程序中第 7~ 12行是一个函数定义(注意第 7行的末尾没有分号)。第 7行定义了一个函数名 max和指定两个形参 x,y及其类型。程序第 4行是一个调用函数语句,max后面括弧内的 a,b是实参。 a和 b是 main
函数中定义的变量,x和 y是函数 max中的形式参数。
通过函数调用,使两个函数中的数据发生联系。见图 8.2。
图 8.2
关于形参与实参的说明:
(1) 在定义函数中指定的形参,在未出现函数调用时,
它们并不占内存中的存储单元。只有在发生函数调用时,函数 max中的形参才被分配内存单元。在调用结束后,形参所占的内存单元也被释放。
(2) 实参可以是常量、变量或表达式,如:
max( 3,a+ b);
但要求它们有确定的值。在调用时将实参的值赋给形参(如果形参是数组名,则传递的是数组首地址而不是数组的值。请参阅 8.7节)。
(3) 在被定义的函数中,必须指定形参的类型(见例
8.2程序第 7行)。
(4) 实参与形参的类型应相同或赋值兼容。例 8.2中实参和形参都是整型,这是合法的、正确的。如果实参为整型而形参 x为实型,或者相反,则按第 2章介绍的不同类型数值的赋值规则进行转换。例如实参值 a为 3.5,而形参 x为整型,则将实数 3.5转换成整数 3,然后送到形参 b。但此时应将 max函数放在 main函数的前面或在 main函数中对被调用函数
max作原型声明,否则会出错。关于对函数的声明见 8.4.3小节。字符型与整型可以互相通用。
(5) C语言规定,实参变量对形参变量的数据传递都是“值传递”,即单向传递,只由实参传给形参,
而不能由形参传回来给实参,这是和FOR TRA
N不同的。在内存中,实参单元与形参单元是不同的单元。如图 8.3所示。
图 8.3 图 8.4
在调用函数时,给形参分配存储单元,并将实参对应的值传递给形参,调用结束后,形参单元被释放,
实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数的实参的值。例如,若在执行函数过程中
x和 y的值变为 10和 15,而 a和 b仍为 2和 3,见图 8.4。
8.3.2 函数的返回值通常,希望通过函数调用使主调函数能得到一个确定的值,这就是函数的返回值。例如,例 8.2中,
max( 2,3)的值是 3,max( 5,2)的值是 5。赋值语句将这个函数值赋给变量 c。下面对函数值作一些说明:
(1) 函数的返回值是通过函数中的 return语句获得的。
return语句将被调用函数中的一个确定值带回主调函数中去。见图 8.2中从 return语句返回的箭头。
如果需要从被调用函数带回一个函数值(供主调函数使用),被调用函数中必须包含 return语句。
如果不需要从被调用函数带回函数值可以不要
return语句。
一个函数中可以有一个以上的 return语句,执行到哪一个 return语句,哪一个语句起作用。
return语句后面的括弧也可以不要,如 return z;
它与,return(z);”等价。
return后面的值可以是一个表达式。例如,例 8.2中的函数 max可以改写如下:
max( int x,int y)
{
return( x> y? x∶ y);
}
这样的函数体更为简短,只用一个 return语句就把求值和返回都解决了。
(2) 函数值的类型。既然函数有返回值,这个值当然应属于某一个确定的类型,应当在定义函数时指定函数值的类型。例如:
int max( float x,float y) /* 函数值为整型 */
char letter( char c1,char c2) /* 函数值为字符型
*/
double min( int x,int y) /* 函数值为双精度型 */
读者会问:例 8.2中的函数定义并没有说明其类型,
为什么?C语言规定,凡不加类型说明的函数,一律自动按整型处理。例 8.2中的 max函数返回值为整型,因此可不必说明。
在定义函数时对函数值说明的类型一般应该和
return语句中的表达式类型一致。例如,例 8.2中用隐含方式指定 max函数值为整型,而变量z也被指定为整型,通过 return语句把z的值作为 max的函数值,由 max带回主调函数。z的类型与 max函数的类型是一致的,是正确的。
(3) 如果函数值的类型和 return语句中表达式的值不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换。即函数类型决定返回值的类型。
例 8.3返回值类型与函数类型不同。将例 8.2稍作改动
(注意是变量的类型改动)。
main()
{
float a,b;
int c;
scanf("% f,% f,",& a,& b);
c= max( a,b);
printf("M axis% d\ n",c);
}
max( float x,float y);
{ float z; /* z为实型变量 */
z= x> y? x∶ y;
return(z);
}
运行情况如下:
1,5,2,5
M ax is 2
函数 max定义为整型,而 return语句中的z为实型,
二者不一致,按上述规定,先将z转换为整型,然后 max( x,y)带回一个整型值 2回主调函数 main。
如果将 main函数中的 c定义为实型,用% f格式符输出,也是输出 2,000000。
有时,可以利用这一特点进行类型转换,如在函数中进行实型运算,希望返回的是整型量,可让系统去自动完成类型转换。但这种做法往往使程序不清晰,可读性降低,容易弄错,而且并不是所有的类型都能互相转换的(如实数与字符类型数据之间)。因此建议初学者不要采用这种方法,而应做到使函数类型与 return返回值的类型一致。
(4) 如果被调用函数中没有 return语句,并不带回一个确定的、用户所希望得到的函数值,但实际上,
函数并不是不带回值,而只是不带回有用的值,
带回的是一个不确定的值。例如,在例 8.1程序中,
尽管没有要求 printstar和 print-message函数带回值,但是如果在程序中出现下面的语句也是合法的:
{ int a,b,c;
a= printstar();
b= print-message();
c= printstar();
printf("% d,% d,% d\ n",a,b,c);
}
运行时除了得到和例 8.1一样的结果外,还可以输出
a,b,c的值(今为 21,20,21)。 a,b,c的值不一定有实际意义(今 printstar函数输出 21个字符,
返回值为 21,print-message输出 20个字符,返回值为 20)。
(5) 为了明确表示“不带回值”,可以用,void”定义
“无类型”(或称“空类型”)。例如,例 8.1中的定义可以改为
void printstar()
{ … }
void print-message()
{ … }
这样,系统就保证不使函数带回任何值,即禁止在调用函数中使用被调用函数的返回值。如果已将
printstar和 print-message函数定义为 void类型,则下面的用法就是错误的:
a= printstar();
b= print-message();
编译时会给出出错信息。
为使程序减少出错,保证正确调用,凡不要求带回函数值的函数,一般应定义为 void类型。许多C语言书的程序中都大量用到 void类型函数,读者应对此有一定了解。
8.4.1 函数调用的一般形式函数调用的一般形式为函数名(实参表列);如果是调用无参函数,则“实参表列”可以没有,但括弧不能省略,见例 8.1。如果实参表列包含多个实参,则各参数间用逗号隔开。实参与形参的个数应相等,类型应一致。实参与形参按顺序对应,
一一传递数据。但应说明,如果实参表列包括多个实参,对实参求值的顺序并不是确定的,有的系统按自左至右顺序求实参的值,有的系统则按自右至左顺序。许多C版本(例如 Turbo C和M
S C)是按自右而左的顺序求值。
8.4 函数的调用例 8.4 main()
{ int i= 2,p;
p= f( i,++ i); /* 函数调用 */
printf("% d",p);
}
int f( int a,int b) /* 函数定义 */
{ int c;
if( a> b) c= 1;
else if( a== b) c= 0;
else c=- 1;
return( c);
}
在 Turbo C系统上运行的结果为,0
如果按自左至右顺序求实参的值,则函数调用相当于 f( 2,3),程序运行应得结果为“- 1”。若按自右至左顺序求实参的值,则它相当于 f( 3,3),
程序运行结果为,0”。读者可以在所用的计算机系统上试一下,以便知道它所处理的方法。由于存在上述情况,使程序通用性受到影响。因此应当避免这种容易引起不同理解的情况。如果本意是按自左而右顺序求实参的值,可以改写为
j= i;
k=++ i;
p= f(j,k);
如果本意是自右而左求实参的值,可改写为
j=++ i;
p= f( j,j);
这种情况在 printf函数中也同样存在,如
printf("% d,% d",i,i++);
也发生上述同样的问题,若 i的原值为 3,在 Turbo C
上运行结果为 4,3。请读者务必注意,应该避免这种容易混淆的用法。
8.4.2 函数调用的方式按函数在程序中出现的位置来分,可以有以下三种函数调用方式:
1,函数语句把函数调用作为一个语句。如例 8.1中的 printstar
();这时不要求函数带回值,只要求函数完成一定的操作。
2,函数表达式函数出现在一个表达式中,这种表达式称为函数表达式。这时要求函数带回一个确定的值以参加表达式的运算。例如,c= 2*max( a,b);函数 max
是表达式的一部分,它的值乘 2再赋给 c。
3,函数参数函数调用作为一个函数的实参。例如,m= max( a,
max( b,c));其中 max( b,c)是一次函数调用,它的值作为 max另一次调用的实参。 m的值是
a,b,c三者最大的。又如,printf ("%d",max
(a,b));也是把 max( a,b)作为 printf函数的一个参数。
函数调用作为函数的参数,实质上也是函数表达式形式调用的一种,因为函数的参数本来就要求是表达式形式。
8.4.3 对被调用函数的声明和函数原型在一个函数中调用另一函数(即被调用函数)需要具备哪些条件呢?
(1) 首先被调用的函数必须是已经存在的函数(是库函数或用户自己定义的函数)。但光有这一条件还不够。
(2) 如果使用库函数,一般还应该在本文件开头用#
include命令将调用有关库函数时所需用到的信息
“包含”到本文件中来。例如,前几章中已经用过的# include <studio,h>其中,studio,h”是一个“头文件”。在 studio,h文件中放了输入输出库函数所用到的一些宏定义信息。如果不包含
,studio,h”文件中的信息,就无法使用输入输出库中的函数。同样,使用数学库中的函数,应该用# include <math,h>.h是头文件所用的后缀,
标志头文件( header file)。有关宏定义等概念请见第 8章。
(3) 如果使用用户自己定义的函数,而且该函数与调用它的函数(即主调函数)在同一个文件中,一般还应该在主调函数中对被调用的函数作声明,
即向编译系统声明将要调用此函数,并将有关信息通知编译系统。“声明” 一词的原文是
declaration,过去在许多书中译为“说明”,近年来,愈来愈多的计算机专家提出应称为声明,
作者也认为称为“声明”更确切,表意更清楚。
例 8.5对被调用的函数作声明。
main()
{ float add( float x,float y);/ *对被调用函数的声明 *
/
float a,b,c;
scanf("% f,% f",& a,& b);
c= add( a,b);
printf(" sum is% f",c);
}
float add( float x,float y) /*函数首部 */
{ float z; /* 函数体 */
z= x+ y;
return(z);
}
运行情况如下:
3.6,6.5
sum is 10.000000
这是一个很简单的函数调用,函数 add的作用是求两个实数之和,得到的函数值也是实型。请注意程序第 2行,float add( float x,float y);是对被调用的 add函数作声明。注意:对函数的“定义”和
“声明”不是一回事。“定义”是指对函数功能的确立,包括指定函数名,函数值类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。而“声明” 的作用则是把函数的名字、
函数类型以及形参的类型、个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致)。
从程序中可以看到对函数的声明与函数定义中的第
1行(函数首部)基本上是相同的。因此可以简单地照写已定义的函数的首部,再加一个分号,就成为了对函数的“声明”。
其实,在函数声明中也可以不写形参名,而只写形参的类型。如,float add(float,float);在 C语言中,
把以上形式的函数声明称为函数原型 (function
prototype)。使用函数原型是 N C的一个重要特点。
它的作用主要是利用它在程序的编译阶段对调用函数的合法性进行全面检查。从例 8.5中可以看到
main函数的位置在 add函数的前面,而在进行如果没有对函数的声明,当编译到包含函数调用的语句,c=add(a,b)”;时,编译系统不知道 add是不是函数名,也无法判断实参( a和 b)的类型和个数是否正确,因而无法进行正确性的检查。只有在运行时才会发现实参与形参的类型或个数不一致,
出现运行错误。但是在运行阶段发现错误并重新调试程序,是比较麻烦的,工作量也较大。
应当在编译阶段尽可能多地发现错误,随之纠正错误。现在我们在函数调用之前用函数原型做了函数声明。因此编译系统记下了所需调用的函数的有关信息,在对,c=add(a,b)”;进行编译时就
“有章可循”了。编译系统根据函数的原型对函数的调用的编译时是从上到下逐行进行的,合法性进行全面的检查。和函数原型不匹配的函数调用会导致编译出错。 它属于语法错误。用户根据屏幕显示的出错信息很容易发现和纠正错误。
函数原型的一般形式为
(1) 函数类型 函数名 (参数类型 1,参数类型 2……)
(2) 函数类型 函数名 (参数类型 1,参数名 1,参数类型 2,参数名 2……)
第 (1)种形式是基本的形式。为了便于阅读程序,也允许在函数原型中加上参数名,就成了第 (2)种形式。但编译系统不检查参数名。因此参数名是什么都无所谓。上面程序中的声明也可以写成
float add(float a,float b);/* 参数名不用 x,y,而用 a,b */
效果完全相同。
应当保证函数原型与函数首部写法上的一致,即函数类型、函数名、参数个数、参数类型和参数顺序必须相同。函数调用时函数名、实参个数应与函数原型一致。实参类型必须与函数原型中的形参类型赋值兼容,按第 2章介绍的赋值规则进行类型转换。如果不是赋值兼容,就按出错处理。
说明:
(1) 以前的 C版本的函数声明方式不是采用函数原型,
而只声明函数名和函数类型。例如在例 8.5中也可以采用下面的函数声明形式
float add( );
不包括参数类型和参数个数。系统不检查参数类型和参数个数。新版本也兼容这种用法,但不提倡这种用法,因为它未进行全面的检查。
(2) 实际上,如果在函数调用之前,没有对函数作声明,则编译系统会把笫一次遇到的该函数形式
(函数定义或函数调用)作为函数的声明,并将函数类型默认为 int型。例如例 8.2在调用 max函数之前没有进行函数声明,编译时首先遇到的函数形式是函数调用,max(a,b)”,由于对原型的处理是不考虑参数名的,因此系统将 max()加上 int作为的函数声明,即
int max( );
因此,不少 C教材说,如果函数类型为整型,可以在函数调用前不必作函数声明。但是使用这种方法时,系统无法对参数的类型做检查。若调用函数时参数使用不当,在编译时也不会报错。因此,为了程序清晰和安全,建议都加以声明为好。例如在例 8.2中最好加上以下函数声明:
int max(int,int);
或
int max(int x,int y);
(3) 如果被调用函数的定义出现在主调函数之前,
可以不必加以声明。因为编译系统已经先知道了已定义的函数类型,会根据函数首部提供的信息对函数的调用作正确性检查。
如果把例 8.5改写如下(即把 main函数放在 add函数的下面),就不必在 main函数中对 add声明。
float add( float x,float y)
{ floatz;
z= x+ y;
return(z);
}
main()/ *不必对 add函数作声明 */
{
float a,b,c;
scanf("% f,% f",& a,& b);
c= add( a,b);
printf("% f",c);
}
( 4) 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必对所调用的函数再作声明 。例如:
char letter( char,char);
/ *以下 3行在所有函数之前,且在函数外部 */
float f( float,float);
int i( float,float);
main()
{ … } /*不必声明它所调用的函数 */
char letter( char c1,char c2) /*定义 letter函数 */
{ … }
float f( float x,float y) /*定义 f函数 */
{ … }
int i( float j,float k) /*定义 i函数 */
{ … }
除了以上 (2)(3)(4) 所提到的三种情况外,都应该按上述介绍的方法对所调用函数作声明,否则编译时就会出现错误。用函数原型来声明函数,还能减少编写程序时可能出现的错误。由于函数声明的位置与函数调用语句的位置比较近,因此在写程序时便于就近参照函数原型来书写函数调用,不易出错。
8.5 函数的嵌套调用
C语言的函数定义都是互相平行、独立的,也就是说在定义函数时,一个函数内不能包含另一个函数,这是和 PASCAL不同的( PASCAL允许在定义一个函数时,
其函数体内又包含另一个函数的完整定义,这就是嵌套定义。这个内嵌的函数只能被 包含它的函数所调用,
其他函数不能调用它)。
C语句不能嵌套定义函数,但可以嵌套调用函数,
也就是说,在调用一个函数的过程中,又调用另一个函数。见图 8.5。
图 8.5
图 8.5表示的是两层嵌套(连 main函数共 3层函数),
其执行过程是:
(1) 执行 main函数的开头部分;
(2) 遇函数调用 a的操作语句,流程转去 a函数;
(3) 执行 a函数的开头部分;
(4) 遇调用 b函数的操作语句,流程转去函数 b;
(5) 执行 b函数,如果再无其他嵌套的函数,则完成 b
函数的全部操作;
(6) 返回调用 b函数处,即返回 a函数;
(7) 继续执行 a函数中尚未执行的部分,直到 a函数结束;
(8) 返回 main函数中调用 a函数处;
(9) 继续执行 main函数的剩余部分直到结束。
例 8.6用弦截法求方程 x3-5x2+16x-80=0的根。
方法如下,
(1) 取两个不同点 x1,x2,如果 f(x1)和 f(x2)符号相反,则
(x1,x2)区间内必有一个根。如果 f(x1)与 f(x2)同符号,
则应改变 x1\,x2,直到 f(x1),f(x2)异号为止。注意 x1、
x2的值不应差太大,以保证 (x1,x2)区间内只有一个根。
(2) 连接 f(x1)和 f(x2)两点,此线 (即弦 )交 x轴于 x,见图
8.6。
x点坐标可用下式求出,
x=x1·f(x2)-x2·f(x1)f(x2)-f(x1)
再从 x求出 f(x)\.
(3) 若 f(x)与 f(x1)同符号,则根必在 (x,x2)区间内,此时将 x作为新的 x1。如果 f(x)与 f(x2)同符号,则表示根在 (x1,x)区间内,将 x作为新的 x2。
图 8.6
图 8.7
(4) 重复步骤 (2) 和 (3),直到 | f(x)|< ε 为止,ε为一个很小的数,例如 10-6,此时认为 f(x)≈0.根据上述思路画出 N-S流程图,见图 8.7。
分别用几个函数来实现各部分功能,
(1) 用函数 f(x)来求 x的函数,x3-5x2+16x-80\.
(2) 用函数 xpoint (x1,x2)来求 f(x1)和 f(x2)的连线与 x
轴的交点 x的坐标。
(3) 用函数 root (x1,x2)来求 (x1,x2)区间的那个实根。
显然,执行 root函数过程中要用到函数 xpoint,而执行 xpoint函数过程中要用到 f函数。
请读者先分析下面的程序。
#include <math.h>
float f(float x) / * 定义 f函数,以实现 f(x)= x3-
5x2+16x-80 */
{
float y;
y=(( x- 5,0) *x+ 16,0) *x- 80,0;
return( y);
}
float xpoint(float x1,float x2) / *定义 xpoint函数,
求出弦与 x轴交点 */
{
float y;
y=( x1*f( x2)- x2*f( x1))/( f( x2)
- f( x1));
return( y);
}
float root( float x1,float x2) /*定义 root函数,求近似根 */
{
int i;
float x,y,y1;
y1= f( x1);
do
{
x= xpoint( x1,x2);
y= f( x);
if( y*y1> 0) f( x)与 f( x1)同符号 */
{ y1= y;
x1= x;}
else
x2= x;
} while( fabs( y)>= 0,0001);
return( x);
}
main() /*主函数 */
{
float x1,x2,f1,f2,x;
do
{
printf(" input x1,x2:\ n");
scanf(“% f,% f”,& x1,& x2);
f1= f( x1);
f2= f( x2);
} while( f1*f2>= 0);
x= root( x1,x2);
printf("A root of equation is% 8,4f",x);
}
运行情况如下:
input x1,x2:
2,6
A root of equation is 5,0000
从程序可以看到:
( 1) 在定义函数时,函数名为 f,xpoint,root的 3个函数是互相独立的,并不互相从属。这 3个函数均定为实型。
( 2) 3个函数的定义均出现在 main函数之前,因此在
main函数中不必对这 3个函数作类型说明。
( 3) 程序从 main函数开始执行。先执行一个 do-while
循环,作用是:输入 x1和 x2,判别 f( x1)和 f( x2)
是否异号,如果不是异号则重新输入 x1和 x2,直到满足 f( x1)与 f( x2)异号为止。然后用函数调用 root
( x1,x2)求根 x。调用 root函数过程中,要调用
xpoint函数来求 f( x1)与 fx2)连线的交点 x。在调用
xpoint函数过程中要用到函数 f来求 x1和 x2的相应的函数值 f( x1)和 f( x2)。这就是函数的嵌套调用。见图 8.8。
图 8.8
(4) 在 root函数中要用到求绝对值的函数 fabs,它是对实型数求绝对值的标准函数。它属于数学函数库,故在文件开头用# include <math,h>即把使用数学库函数时所需用到的有关信息包含进来。
8.6 函数的递归调用在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。C语言的特点之一就在于允许函数的递归调用。例如:
int f( int x)
{ int y,z;
z= f( y);
return( 2*z);
}
在调用函数 f的过程中,又要调用 f函数,这是直接调用本函数,见图 8.9。下面是间接调用本函数。
图 8.9 图 8.10
在调用 f1函数过程中要调用 f2函数,而在调用 f2函数过程中又要调用 f1函数,见图 8.10。
从图上可以看到,这两种递归调用都是无终止的自身调用。显然,程序中不应出现这种无终止的递归调用,而只应出现有限次数的、有终止的递归调用,这可以用 if语句来控制,只有在某一条件成立时才继续执行递归调用,否则就不再继续。
关于递归的概念,有些初学者感到不好理解,下面用一个通俗的例子来说明。
例 8.7 有 5个人坐在一起,问第 5个人多少岁?他说比第 4个人大 2岁。问第 4个人岁数,他说比第 3个人大 2岁。问第 3个人,又说比第 2个人大 2岁。问第 2个人,说比第 1个人大 2岁。最后问第 1个人,
他说是 10岁。请问第 5个人多大。显然,这是一个递归问题。要求第 5个人的年龄,就必须先知道第
4个人的年龄,而第 4个人的年龄也不知道,要求第 4个人的年龄必须先知道第 3个人的年龄,而第 3
个人的年龄又取决于第 2个人的年龄,第 2个人的年龄取决于第 1个人的年龄。而且每一个人的年龄都比其前 1个人的年龄大 2。
即 age( 5)= age( 4)+ 2
age( 4)= age( 3)+ 2
age( 3)= age( 2)+ 2
age( 2)= age( 1)+ 2
age( 1)= 10
可以用式子表述如下:
age( n)= 10 ( n= 1)
age( n- 1)+ 2 ( n> 1)
可以看到,当 n> 1时,求第 n个人的年龄的公式是相同的。因此可以用一个函数表示上述关系。图 8.11
表示求第 5个人年龄的过程。
图 8.11
从图可知,求解可分成两个阶段:第一阶段是“回推”,即将第 n个人的年龄表示为第( n- 1)个人年龄的函数,而第( n- 1)个人的年龄仍然不知道,还要“回推”到第( n- 2)个人的年龄 ……
直到第 1个人年龄。此时 age( 1)已知,不必再向前推了。
然后开始第二阶段,采用递推方法,从第 1个人的已知年龄推算出第 2个人的年龄( 12岁),从第 2个人的年龄推算出第 3个人的年龄( 14岁) …… 一直推算出第 5个人的年龄( 18岁)为止。也就是说,
一个递归的问题可以分为“回推”和“递推”两个阶段。要经历许多步才能求出最后的值。显而易见,如果要求递归过程不是无限制进行下去,
必须具有一个结束递归过程的条件。例如,age
( 1)= 10,就是使递归结束的条件。
可以用一个函数来描述上述递归过程:
age( int n) /*求年龄的递归函数 */
{int c; /* c用作存放函数的返回值的变量 */
if( n== 1) c= 10;
else c= age( n- 1)+ 2;
return( c);
}
main()
{
printf( "% d",age( 5));
}
运行结果如下:
18
main函数中只有一个语句。整个问题的求解全靠一个 age( 5)函数调用来解决。函数调用过程如图
8.12所示。
图 8.12
从图 8.12可以看到,age函数共被调用 5次,即 age
( 5),age( 4),age( 3),age( 2),age
( 1)。其中 age( 5)是 main函数调用的,其余 4
次是在 age函数中调用的,即递归调用 4次。请读者仔细分析调用的过程。应当强调说明的是在某一次调用 age函数时并不是立即得到 age( n)的值,
而是一次又一次地进行递归调用,到 age( 1)时才有确定的值,然后再递推出 age( 2),age( 3)、
age( 4),age( 5)。请读者将程序和图 8.11、图
8.12结合起来认真分析。
例 8.8用递归方法求 n!。
求 n!可以用递推方法,即从 1开始,乘 2,再乘
3…… 一直乘到 n。这种方法容易理解,也容易实现。
递推法的特点是从一个已知的事实出发,按一定规律推出下一个事实,再从这个新的已知的事实出发,
再向下推出一个新的事实 …… 这是和递归不同的。
求 n! ··也可以用递归方法,即 5!等于 4! × 5,而 4!
= 3! × 4…1 != 1。可用下面的递归公式表示:
n!= 1( n= 0,1)
n·( n- 1)!( n> 1)
有了例 8.7的基础,很容易写出本题的程序:
float fac( int n)
{
float f;
if( n< 0) { printf( "n< 0,dataerror! "); f=-1; }
else if( n== 0‖ n== 1) f= 1;
else f= fac( n- 1) *n;
return( f);
}
main()
{
int n;
float y;
printf( "input a integer number,");
scanf( "% d",& n);
y= fac( n);
printf( "% d!=% 15,0f",n,y);
}
运行情况如下:
input a integer number,10
10!= 3628800.
例 8.9hanoi(汉诺)塔问题。这是一个古典的数学问题,是一个只有用递归方法(而不可能用其他方法)解决的问题。问题是这样的:古代有一个梵塔,塔内有 3个座 A,B,C,开始时A座上有 64个盘子,盘子大小不等,大的在下,小的在上(图
8.13)。有一个老和尚想把这 64个盘子从A座移到
C座,但每次只允许移动一个盘,且在移动过讨性?个座上都始终保持大盘在下,小盘在上。在移动过程中可以利用B座,要求编程序打印出移动的步骤。
图 8.13
可以肯定地说:任何一个人(包括“天才”) 都不可能直接写出移动盘子的每一个具体步骤。请读者试验一下按上面的规律将 5个盘子从 A座移到 C座,能否直接写出每一步骤?老和尚自然会这样想:假如有另外一个和尚能有办法将 63个盘子从一个座移到另一座。
那么,问题就解决了。此时老和尚只需这样做:
(1) 命令第 2个和尚将 63个盘子从 A座移到 B座;
(2) 自己将 1个盘子(最底下的、最大的盘子)从 A座移到 C座;
(3) 再命令第 2个和尚将 63个盘子从 B座移到 C座。
至此,全部任务完成了。这就是递归方法。但是,
有一个问题实际上未解决:第 2个和尚怎样才能将
63个盘子从 A座移到 B座?为了解决将 63个盘子从
A座移到 B座,第 2个和尚又想:如果有人能将 62个盘子从一个座移到另一座,我就能将 63个盘子从 A
座移到 B座,他是这样做的:
(1) 命令第 3个和尚将 62个盘子从 A座移到 C座;
(2) 自己将 1个盘子从 A座移到 B座;
(3) 再命令第 3个和尚将 62个盘子从 C座移到 B座。
再进行一次递归。如此“层层下放”,直到后来找到第 63个和尚,让他完成将 2个盘子从一个座移到另一座,进行到此,问题就接近解决了。最后找到第 64个和尚,让他完成将 1个盘子从一个座移到另一座,至此,全部工作都已落实,都是可以执行的。可以看出,递归的结束条件是最后一个和尚只需移一个盘子。否则递归还要继续进行下去。
应当说明,只有第 64个和尚的任务完成后,第 63个和尚的任务才能完成。只有第 2到第 64个和尚任务完成后,第 1个和尚的任务才能完成。这是一个典型的递归的问题。为使问题简化,我们先分析将
A座上 3个盘子移到C座上的过程:
(1) 将A座上 2个盘子移到B座上(借助C);
(2) 将A座上 1个盘子移到C座上;
(3) 将B座上 2个盘子移到C座上(借助A)。
其中第 2步可以直接实现。第 1步又可用递归方法分解为:
1,1将A上 1个盘子从A移到C;
1,2将A上 1个盘子从A移到B;
1,3将C上 1个盘子从C移到B。
第 3步可以分解为:
3,1将B上 1个盘子从B移到A上;
3,2将B上 1个盘子从B移到C上;
3,3将A上 1个盘子从A移到C上。
将以上综合起来,可得到移动 3个盘子的步骤为
A → C,A → B,C → B,A → C,B → A,B →
C,A → C。
共经历 7步。由此可推出:移动 n个盘子要经历 2n-1
步。如移 4个盘子经历 15步,移 5个盘子经历 31步,
移 64个盘子经历 264-1步。
由上面的分析可知:将 n个盘子从A座移到C座可以分解为以下 3个步骤:
(1) 将A上 n- 1个盘借助C座先移到B座上。
(2) 把A座上剩下的一个盘移到C座上。
(3) 将 n- 1个盘从B座借助于A座移到C座上。
上面第 1步和第 3步,都是把 n- 1个盘从一个座移到另一个座上,采取的办法是一样的,只是座的名字不同而已。为使之一般化,可以将第 1步和第 3
步表示为:
“将,one” 座上 n- 1个盘移到,two” 座 (借助,three”
座 )。只是在第①步和第③步中,one,two,three
和A、B、C的对应关系不同。对第①步,对应关系是 one——A,two——B,three——C。对第③步,是,one——B,two——C,three——
A。
因此,可以把上面 3个步骤分成两类操作:
(1) 将 n- 1个盘从一个座移到另一个座上( n> 1)。
这就是大和尚让小和尚做的工作,它是一个递归的过程,即和尚将任务层层下放,直到第 64个和尚为止。
(2) 将 1个盘子从一个座上移到另一座上。这是大和尚自己做的工作。
下面编写程序。分别用两个函数实现以上的两类操作,用 hanoi函数实现上面第 1类操作(即模拟小和尚的任务),用 move函数实现上面第 2类操作(模拟大和尚自己移盘),函数调用 hanoi( n,one,two,three)表示
“将 n个盘子从,one” 座移到,three” 座的过程 (借助,two”针” )。函数调用 move( x,y)表示将 1个盘子从 x 座移到 y 座的过程。 x和 y是代表A、B、
C座之一,根据每次不同情况分别取A、B、C代入。
程序如下:
void move( char x,char y)
{
printf( "% c-->% c\ n",x,y);
}
void hanoi( int n,char one,char two,char
three)
/*将 n个盘从 one座借助 two座,移到 three座 */
{
if( n== 1) move( one,three);
else
{
hanoi( n- 1,one,three,two);
move( one,three);
hanoi( n- 1,two,one,three);
}
}
main()
{
int m;
printf( "input the number of diskes,");
scanf( "% d",& m);
printf( "The step to moving % 3d diskes:\ n",
m);
hanoi( m,′A ′,′B ′,′C ′);
}
运行情况如下:
input the number of diskes,3
The step to moving 3 diskes:
A-->C
A-->B
C-->B
A-->C
B-->A
B-->C
A-->C
在本程序中 move函数并未真正移动盘子,而只是打印出移盘的方案(从哪一个座移到哪一个座)。
由于篇幅关系,不再对上述程序做过多解释,请读者仔细理解。
8.7 数组作为函数参数前面已经介绍了可以用变量作函数参数,此外,数组元素也可以作函数实参,其用法与变量相同。数组名也可以作实参和形参,传递的是整个数组。
1,数组元素作函数实参由于实参可以是表达式形式,数组元素可以是表达式的组成部分,因此数组元素当然可以作为函数的实参,与用变量作实参一样,是单向传递,即“值传送”方式。
例 8.10有两个数组 a,b,各有 10个元素,将它们对应地逐个相比(即 a[ 0]与 b[ 0]比,a[ 1]与 b[ 1]
比 …… )。如果 a数组中的元素大于 b数组中的相应元素的数目多于 b数组中元素大于 a数组中相应元素的数目 (例如,a[ i] >b[ i] 6次,b[ i]
>a[ i] 3次,其中 i每次为不同的值 ),则认为 a数组大于 b数组,并分别统计出两个数组相应元素大于、等于、小于的次数。
程序如下:
main()
{ int large(int x,int y); /*函数声明 */
int a[ 10],b[ 10],i,n= 0,m= 0,k= 0;
printf(,enter array a∶ \ n”);
for( i= 0; i< 10; i++)
scanf( "% d",& a[ i]);
printf( "\ n");
printf( "enter array b∶ \ n");
for( i= 0; i< 10; i++)
scanf( "% d",& b[ i]);
printf( "\ n");
for( i= 0; i< 10; i++)
{if( large( a[ i],b[ i])== 1) n= n+ 1;
else if( large( a[ i],b[ i])== 0) m= m+ 1;
else k= k+ 1; }
printf("a[i]>b[i]%d times\ na[i]=b[i]%d times
\ na[i]<b[i]%d times\ n",n,m,k);
if(n>k) printf("array a is larger than array b\
n");
else if (n<k) printf("array a is smaller than
array b\ n");
else printf("array a is equal to array
b\ n");
}
large( int x,int y)
{int flag;
if( x> y) flag= 1;
else if( x< y) flag=- 1;
else flag= 0;
return( flag);
}
运行情况如下:
enter array a:
1 3 5 7 9 8 6 4 2 0
enter array b∶
5 3 8 9-1-3 5 6 0 4
a[ i]> b[ i] 4 times
a[ i]= b[ i] 1 times
a[ i]< b[ i] 5 times
array a is smaller than array b
2,数组名可作函数参数可以用数组名作函数参数,此时实参与形参都应用数组名(或用指针变量,见第 9章)。
例 8.11有一个一维数组 score,内放 10个学生成绩,
求平均成绩。
程序如下:
float average( float array[ 10])
{ int i;
float aver,sum= array[ 0];
for( i= 1; i< 10; i++)
sum= sum+ array[ i];
aver= sum/ 10;
return( aver);
}
main()
{ float score[ 10],aver;
int i;
printf( "input 10 scores:\ n");
for( i= 0; i< 10; i++)
scanf( "% f",& score[ i]);
printf( "\ n");
aver= average( score);
printf( "average score is % 5,2f",aver);
}
运行情况如下:
input 10 scores:
100567898,576879967,57597
average score is 83,40
说明:
(1) 用数组名作函数参数,应该在主调函数和被调用函数分别定义数组,例中 array是形参数组名,
score是实参数组名,分别在其所在函数中定义,
不能只在一方定义。
(2) 实参数组与形参数组类型应一致(今都为 float
型),如不一致,结果将出错。
(3) 在被调用函数中声明了形参数组的大小为 10,但在实际上,指定其大小是不起任何作用的,因为
C编译对形参数组大小不做检查,只是将实参数组的首地址传给形参数组。因此,score[ n]和
array[ n]指的是同一单元。
(4) 形参数组也可以不指定大小,在定义数组时在数组名后面跟一个空的方括弧,为了在被调用函数中处理数组元素的需要,可以另设一个参数,传递数组元素的个数,例 8.11可以改写为例 8.12形式。
例 8.12
float average( float array[ ]
,int n)
{ int i;
float aver,sum= array[ 0];
for( i= 1; i< n; i++)
sum= sum+ array[ i];
aver= sum/ n;
return( aver);
}
main()
{ float score-1[ 5]={ 98,5,97,91,5,60,
55};
float score-2[ 10] ={67.5,89.5,99,69.5,77,
89.5,76.5,54,60,99.5};
printf("the average of class A is %6.2f\ n",
average(score-1,5));
printf("the average of class B is %6.2f\ n",
average(score-2,10));
}
运行结果如下:
the average of class A is 80,40
the average of class B is 78,20
可以看出,两次调用 average函数时需要处理的数组元素个数是不同的,在第一次调用时用一个实参 5
传递给形参 n,表示求前面 5个学生的平均分。第 2次调用时,求 10个学生平均分。
(5) 最后应当说明一点,用数组名作函数实参时,不是把数组的值传递给形参,而是把实参数组的起始地址传递给形参数组,这样两个数组就共占同一段内存单元。见图 8.14。假若 a的起始地址为 1000,则 b数组的起始地址也是 1000,显然,a和 b同占一段内存单元,a[ 0]与 b[ 0]同占一个单元 …… 。由此可以看到,形参数组中各元素的值如发生变化会使实参数组元素的值同时发生变化,从图 8.14看是很容易理解的。这一点是与变量做函数参数的情况不相同的,务请注意。在程序设计中可以有意识地利用这一特点改变实参数组元素的值 (如排序 )。
关于数组名作为函数参数,将在第 9章介绍完指针变量后作进一步的说明。
起始地址 1000
a[ 0] a[ 1] a[ 2]
a[ 3] a[ 4] a[ 5] a[ 6] a[ 7] a[ 8] a[ 9]
2468101214161820
b[ 0] b[ 1] b[ 2] b[ 3] b[ 4] b[ 5] b[ 6] b
[ 7] b[ 8] b[ 9]
图 8.14
例 8.13
用选择法对数组中 10个整数按由小到大排序。所谓选择法就是先将 10个数中最小的数与 a[ 0]对换 ;再将
a[ 1]到 a[ 9]中最小的数与 a[ 1]对换 …… 每比较一轮,找出一个未经排序的数中最小的一个。共比较 9轮。
下面以 5个数为例说明选择法的步骤。
a[ 0] a[ 1] a[ 2] a[ 3] a[ 4]
3 6 1 9 4
未排序时的情况
1 6 3 9 4 将 5个数中最小的数 1与 a[ 0]
对换
1 3 6 9 4 将余下的 4个数中最小的数 3与 a
[ 1]对换
1 3 4 9 6 将余下的 3个数中最小的数 4与 a
[ 2]对换
1 3 4 6 9 将余下的 2个数中最小的数 6与 a
[ 3]对换,至此完成排序根据此思路编写程序如下:
void sort( int array[],int n)
{ int i,j,k,t;
for( i= 0; i< n- 1; i++)
{ k= i;
for(j= i+ 1;j< n;j++)
if( array[j]< array[ k]) k
=j;
t= array[ k]; array[ k]= array
[ i]; arra
y[ i]= t;}
}
main()
{ int a[ 10],i;
printf( "enter the array\ n");
for( i= 0; i< 10; i++)
scanf( "% d",& a[ i]);
sort( a,10);
printf( "the sorted array∶ \ n");
for( i= 0; i< 10; i++)
printf( "% d",a[ i]);
printf( "\ n");
}
可以看到在执行函数调用语句 sort( a,10);之前和之后,a数组中各元素的值是不同的。原来是无序的,执行 sort( a,10);后,a数组已经排好序了,这是由于形参数组 array已用选择法进行排序了,形参数组改变也使实参数组随之改变。
3,用多维数组名作函数参数多维数组元素可以作为实参,这点与前述相同。
可以用多维数组名作为实参和形参,在被调用函数中对形参数组定义时可以指定每一维的大小,也可以省略第一维的大小说明。如 int array[ 3]
[ 10];或 int array[ ][ 10];二者都合法而且等价。但是不能把第二维以及其他高维的大小说明省略。如下面是不合法的:
int array[ ][ ];
因为从实参传送来的是数组起始地址,在内存中各元素是一行接一行地顺序存放的,而并不区分行和列,
如果在形参中不说明列数,则系统无法决定应为多少行多少列。不能只指定第一维而省略第二维,下面写法是错误的:
int array[ 3][];
形参数组第一维的大小可以是任意的。例如,实参数组定义为 int score[ 5][ 10];而形参数组定义为
int array[ 3][ 10];均可以,C编译不检查第一维的大小。请读者从“传递地址”这一特点出发来思考这个问题。
例 8.14有一个 3× 4的矩阵,求所有元素中的最大值。
解此题的算法是:先使变量 max的初值为矩阵中第一个元素的值,然后将矩阵中各个元素的值与 max相比,每次比较后都把“大者”存放在 max中,全部元素比较完后,max 的值就是所有元素的最大值。
程序如下:
max-value( int array[ ][ 4])
{ int i,j,max;
max= array[ 0][ 0];
for( i= 0; i< 3; i++)
for(j= 0;j< 4;j++)
if( array[ i][j]> max) max= array[ i]
[ j];
return( max);
}
main()
{ int a[ 3][ 4]={{ 1,3,5,7},{ 2,4,
6,8},{ 15,17,34,12}};
printf( "max value is% d\ n",max-value
( a));
}
运行结果如下:
max value is 34
8.8.1 局部变量在一个函数内部定义的变量是内部变量,它只在本函数范围内有效,也就是说只有在本函数内才能使用它们,在此函数以外是不能使用这些变量的。
这称为“局部变量”。如:
float f1( int a)/*函数 f1*/
{int b,c;
… a,b,c有效
}
char f2(int x,int y)/*函数 f2*/
8.8 局部变量和全局变量
{int i,j;
} x,y,i,j有效
main( )/*主函数 */
{int m,n;
… m,n有效
}
说明,
(1) 主函数 main中定义的变量 (m,n)也只在主函数中有效,而不因为在主函数中定义而在整个文件或程序中有效,这是和 PASCAL不同的。主函数也不能使用其他函数中定义的变量。
(2) 不同函数中可以使用相同名字的变量,它们代表不同的对象,互不干扰。例如,在 f1函数中定义了变量
b,c,倘若在 f2函数中也定义变量 b和 c,它们在内存中占不同的单元,互不混淆。
(3) 形式参数也是局部变量。例如 f1函数中的形参 a,
也只在 f1函数中有效。其他函数不能调用。
(4) 在一个函数内部,可以在复合语句中定义变量,这些变量只在本复合语句中有效,这种复合语句也可称为“分程序”或“程序块”。
main ( )
{int a,b;
…
{int c;
c=a+b; c在此范围内有效 a,b在此范围内有效
…
}
…
}
变量 c只在复合语句 (分程序 )内有效,离开该复合语句该变量就无效,释放内存单元。
8.8.2 全局变量前已介绍,程序的编译单位是源程序文件,一个源文件可以包含一个或若干个函数 \.在函数内定义的变量是局部变量,而在函数之外定义的变量称为外部变量,外部变量是全局变量 (也称全程变量 )。全局变量可以为本文件中其他函数所共用。它的有效范围为从定义变量的位置开始到本源文件结束。如,
int p=1,q=5;/ /
float f1(a)/ 定义函数 f1 /
int a;
{int b,c;
…
}
char c1,c2;/ / 全局变量 p,q
char f2 (int x,int y)/*定义函数 f2/ 的作用范围
{int i,j; 全局变量 c1,
… c2 的作用范围
}
main ( )/ /
{int m,n;
…
}
p,q,c1,c2都是全局变量,但它们的作用范围不同,在 main函数和 f2函数中可以使用全局变量 p、
q,c1,c2,但在函数 f1中只能使用全局变量 p,q,
而不能使用 c1和 c2。
在一个函数中既可以使用本函数中的局部变量,又可以使用有效的全局变量。打个通俗的比方:国家有统一的法律和法令,各省还可以根据需要制定地方的法律、法令。在甲省,国家统一的法律法令和甲省的法律法令都是有效的,而在乙省,
则国家统一的和乙省的法律法令有效。显然,甲省的法律法令在乙省无效。
说明:
(1) 设全局变量的作用是增加了函数间数据联系的渠道。由于同一文件中的所有函数都能引用全局变量的值,因此如果在一个函数中改变了全局变量的值,就能影响到其他函数,相当于各个函数间有直接的传递通道。由于函数的调用只能带回一个返回值,因此有时可以利用全局变量增加与函数联系的渠道,从函数得到一个以上的返回值。
为了便于区别全局变量和局部变量,在 C程序设计人员中有一个不成文的约定(但非规定),将全局变量名的笫一个字母用大写表示。
例 8.15有一个一维数组,内放 10个学生成绩,写一个函数,求出平均分,最高分和最低分。
显然希望从函数得到 3个结果值,除了可得到一个函数的返回值以外,还可以利用全局变量。
float Max= 0,Min= 0
float average( float array[ ],int n) / * 定义函数,形参为数组 */
{ int i;
float aver,sum= array[ 0];
Max= Min= array[ 0];
for( i= 1; i< n; i++)
{ if( array[ i]> Max) Max= array[ i];
else if( array[ i]< Min) Min= array[ i];
sum= sum+ array[ i];
}
aver= sum/ n;
return( aver);
}
main()
{ float ave,score[ 10];
int i;
for( i= 0; i< 10; i++)
scanf( "% f",& score[ i]);
ave= average( score,10);
printf(“max=%6.2f \ nmin=%6.2f\
naverage=%6.2f\ n”,Max,Min,ave);
}
图 8.15
运行情况如下:
99 45 78 97 100 67,5 89 92 66 43
max= 100,00
min= 43,00
average= 77,65
函数 average中与外界有联系的变量与外界的联系如图
8.15所示。可以看出形参 array和 n的值由 main函数传递给形参,函数 average中 aver的值通过 return语句带回
main函数。 Max和 Min是全局变量,是公用的它的值可以供各函数使用,如果在一个函数中,改变了它们的值,在其他函数中也可以使用这个已改变的值。由此看出,可以利用全局变量以减少函数实参与形参的个数,从而减少内存空间以及传递数据时的时间消耗。
(2) 建议不在必要时不要使用全局变量,因为:
① 全局变量在程序的全部执行过程中都占用存储单元,
而不是仅在需要时才开辟单元。
② 它使函数的通用性降低了,因为函数在执行时要依赖于其所在的外部变量。如果将一个函数移到另一个文件中,还要将有关的外部变量及其值一起移过去。但若该外部变量与其他文件的变量同名时,就会出现问题,降低了程序的可靠性和通用性。在程序设计中,
在划分模块时要求模块的“内聚性”强、与其他模块的“耦合性”弱。即模块的功能要单一(不要把许多互不相干的功能放到一个模块中),与其他模块的相互影响要尽量少,而用全局变量是不符合这个原则的。
一般要求把C程序中的函数做成一个封闭体,除了可以通过“实参 ——形参”的渠道与外界发生联系外,
没有其他渠道。这样的程序移植性好,可读性强。
③ 使用全局变量过多,会降低程序的清晰性,人们往往难以清楚地判断出每个瞬时各个外部变量的值。在各个函数执行时都可能改变外部变量的值,
程序容易出错。因此,要限制使用全局变量。
(3) 如果在同一个源文件中,外部变量与局部变量同名,则在局部变量的作用范围内,外部变量被
“屏蔽”,即它不起作用。如:
例 8.16外部变量与局部变量同名。
int a=3,b=5;/* a,b */a,b作用范围
max (int a,int b)/ a,b /
{int c;
c=a> b?a∶ b; 形参 a,b作用范围
return (c);
}
main( )
{int a=8; / a /局部变量 a作用范围
printf (“%d”,max (a,b)); 全局变量 b的作用范围
}
运行结果为
8
我们故意重复使用 a,b作变量名,请读者区别不同的 a,b的含义和作用范围。第 1行定义了外部变量
a,b,并使之初始化。第 2行开始定义函数 max,
a和 b是形参,形参也是局部变量。函数 max中的 a、
b不是外部变量 a,b,它们的值是由实参传给形参的,外部变量 a,b在 max函数范围内不起作用。
最后 4行是 main函数,它定义了一个局部变量 a,
因此全局变量 a在 main函数范围内不起作用,而全局变量 b在此范围内有效。因此 printf函数中的
max( a,b)相当于 max( 8,5),程序运行后得到结果为 8。
8.9.1 动态存储方式与静态存储方式上一节已介绍了,从变量的作用域(即从空间)
角度来分,可以分为全局变量和局部变量。
可以从另一个角度,从变量值存在的时间(即生存期)角度来分,可以分为静态存储方式和动态存储方式。
所谓静态存储方式是指在程序运行期间分配固定的存储空间的方式。而动态存储方式则是在程序运行期间根据需要进行动态的分配存储空间的方式。
8.9 变量的存储类别用户区程序区静态存储区动态存储区图 8.16
先看一下内存中的供用户使用的存储空间的情况。
这个存储空间可以分为三部分,见图 8.16。
1,程序区
2,静态存储区
3,动态存储区数据分别存放在静态存储区和动态存储区中。全局变量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储区,程序执行完毕就释放。
在程序执行过程中它们占据固定的存储单元,而不是动态地进行分配和释放。
在动态存储区中存放以下数据:
①函数形式参数。在调用函数时给形参分配存储空间。
②自动变量(未加 static声明的局部变量,详见后面的介绍)。
③函数调用时的现场保护和返回地址等。对以上这些数据,在函数调用开始时分配动态存储空间,
函数结束时释放这些空间。在程序执行过程中,
这种分配和释放是动态的,如果在一个程序中两次调用同一函数,分配给此函数中局部变量的存储空间地址可能是不相同的。如果一个程序包含若干个函数,每个函数中的局部变量的生存期并不等于整个程序的执行周期,它只是程序执行周期的一部分。根据函数调用的需要,动态地分配和释放存储空间。
在C语言中每一个变量和函数有两个属性:数据类型和数据的存储类别。数据类型,读者已熟悉
(如整型、字符型等)。存储类别指的是数据在内存中存储的方法。存储方法分为两大类:静态存储类和动态存储类。具体包含四种:自动的
( auto),静态的( static),寄存器的
( register),外部的( xtern)。根据变量的存储类别,可以知道变量的作用域和生存期。
下面分别作介绍。
8.9.2 auto变量函数中的局部变量,如不专门声明为 static存储类别,
都是动态地分配存储空间的,数据存储在动态存储区中。函数中的形参和在函数中定义的变量 (包括在复合语句中定义的变量),都属此类,在调用该函数时系统会给它们分配存储空间,在函数调用结束时就自动释放这些存储空间。因此这类局部变量称为自动变量。自动变量用关键字 auto
作存储类别的声明。例如:
int f( int a) /*定义 f函数,a为形参 */
{ auto int b,c= 3; /*定义 b,c为自动变量 */
…
}
a是形参,b,c是自动变量,对 c赋初值 3。执行完 f
函数后,自动释放 a,b,c 所占的存储单元。
实际上,关键字,auto”可以省略,auto不写则隐含确定为“自动存储类别”,它属于动态存储方式。
程序中大多数变量属于自动变量。我们前面介绍的函数中定义的变量都没有声明为 auto,其实都隐含指定为自动变量。例如,在函数体中:
auto int b,c= 3;
int b,c= 3;
二者等价
8.9.3 用 static声明局部变量有时希望函数中的局部变量的值在函数调用结束后不消失而保留原值,即其占用的存储单元不释放,
在下一次该函数调用时,该变量已有值,就是上一次函数调用结束时的值。这时就应该指定该局部变量为“静态局部变量”,用关键字 static进行声明。通过下面简单的例子可以了解它的特点。
例 8.17考察静态局部变量的值。
f( int a)
{ auto b= 0;
static c= 3;
b= b+ 1;
c= c+ 1;
return( a+ b+ c);
}
main()
{ int a= 2,i;
for( i= 0; i< 3; i+
+)
printf( "% d ",f
( a));
}
运行结果为:
7 8 9
图 8.17
在第 1次调用 f函数时,b的初值为 0,c的初值为 3,
第 1次调用结束时,b= 1,c= 4,a+ b+ c= 7。由于 c是静态局部变量,在函数调用结束后,它并不释放,仍保留 c= 4。在第 2次调用 f函数时,b的初值为 0,而 c的初值为 4(上次调用结束时的值)。
见图 8.17。
对静态局部变量的说明:
(1) 静态局部变量属于静态存储类别,在静态存储区内分配存储单元。在程序整个运行期间都不释放。
而自动变量(即动态局部变量)属于动态存储类别,占动态存储区空间而不占静态存储区空间,
函数调用结束后即释放。
(2) 对静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的值。而对自动变量赋初值,不是在编译时进行的,而是在函数调用时进行,,每调用一次函数重新给一次初值,相当于执行一次赋值语句。
(3) 如在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动赋初值 0(对数值型变量)
或空字符(对字符变量)。而对自动变量来说,
如果不赋初值则它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另分配存储单元,而所分配的单元中的值是不确定的。
(4) 虽然静态局部变量在函数调用结束后仍然存在,
但其他函数是不能引用它的。
在什么情况下需要用局部静态变量呢?
(1) 需要保留函数上一次调用结束时的值。例如可以用下面方法求 n!。
例 8.18打印 1到 5的阶乘值。
int fac( int n)
{ static int f= 1;
f= f*n;
return( f);
}
main()
{ int i;
for( i= 1; i<= 5; i++)
printf( "% d!=% d\ n",i,fac( i));
}
运行结果为:
1!= 1
2!= 2
3!= 6
4!= 24
5!= 120
每次调用 fac( i),打印出一个 i!,同时保留这个 i!
的值以便下次再乘( i+ 1)。
(2) 如果初始化后,变量只被引用而不改变其值,则这时用静态局部变量比较方便,以免每次调用时重新赋值。
但是应该看到,用静态存储要多占内存(长期占用不释放,而不能像动态存储那样一个存储单元可供多个变量使用,节约内存),而且降低了程序的可读性,当调用次数多时往往弄不清静态局部变量的当前值是什么。因此,如不必要,不要多用静态局部变量。
8.9.4 register变量一般情况下,变量(包括静态存储方式和动态存储方式)的值是存放在内存中的。当程序中用到哪一个变量的值时,由控制器发出指令将内存中该变量的值送到运算器中。 经过运算器进行运算,如果需要存数,再从运算器将数据送到内存存放。见图
8.18。
图 8.18
如果有一些变量使用频繁(例如在一个函数中执行
10000次循环,每次循环中都要引用某局部变量),
则为存取变量的值要花不少时间。为提高执行效率,
C语言允许将局部变量的值放在 CPU中的寄存器中,
需要用时直接从寄存器取出参加运算,不必再到内存中去存取。由于对寄存器的存取速度远高于对内存的存取速度,因此这样做可以提高执行效率。这种变量叫做“寄存器变量”,用关键字 register作声明。例如,例 8.19是输出 1到 5的阶乘的值。
例 8.19使用寄存器变量。
int fac( int n)
{ register int i,f= 1;/ *定义寄存器变量 */
for( i= 1; i<= n; i++)
f= f*i;
return( f);
}
main()
{ int i;
for( i= 1; i<= 5; i++)
printf( "% d!=% d\ n",i,fac( i));
}
定义局部变量 f和 i是寄存器变量,如果 n的值大,则能节约许多执行时间。
说明:
(1) 只有局部自动变量和形式参数可以作为寄存器变量,其他(如全局变量)不行。在调用一个函数时占用一些寄存器以存放寄存器变量的值,函数调用结束释放寄存器。此后,在调用另一个函数时又可以利用它来存放该函数的寄存器变量。
(2) 一个计算机系统中的寄存器数目是有限的,不能定义任意多个寄存器变量。不同的系统允许使用的寄存器是不同的,而且对 register变量的处理方法也是不同的,有的系统对 register变量当作自动变量处理,分配内存单元,并不真正把它们存放在寄存器中,有的系统只允许将 int,char和指针型变量定义为寄存器变量。
当今的优化编译系统能够识别使用频繁的变量,从而自动地将这些变量放在寄存器中,而不需要程序设计者指定。因此在实际上用 register声明变量是不必要的。读者对它有一定了解即可。
(3) 局部静态变量不能定义为寄存器变量。不能写成
register static int a,b,c;不能把变量 a,b,c既放在静态存储区中,又放在寄存器中,二者只能居其一。对一个变量只能声明为一个存储类别。
8.9.5 用 extern声明外部变量外部变量(即全局变量)是在函数的外部定义的,
它的作用域为从变量的定义处开始,到本程序文件的末尾。在此作用域内,全局变量可以为程序中各个函数所引用。编译时将外部变量分配在静态存储区。有时需要用 extern来声明外部变量,以扩展外部变量的作用城。
1,在一个文件内声明外部变量如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件终了。如果在定义点之前的函数想引用该外部变量,则应该在引用之前用关键字 extern对该变量作“外部变量声明”。表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。例如:
例 8.20用 extern声明外部变量,扩展程序文件中的作用域。
int max( int x,int y max
{ int z;
z= x> y? x∶ y;
return(z);
}
main()
{ extern A,B; /*外部变量声明 */
printf( "% d",max( A,B));
}
int A= 13,B=- 8;
/
运行结果如下,13
在本程序文件的最后 1行定义了外部变量 A,B,但由于外部变量定义的位置在函数 main之后,因此本来在 main函数中不能引用外部变量 A和 B。现在我们在 main函数的第 2行用 extern对 A和 B进行“外部变量声明”,表示 A和 B是已经定义的外部变量
(但定义的位置在后面)。这样在 main函数中就可以合法地使用全局变量 A和 B了。如果不作
extern声明,编译时出错,系统不会认为 A,B是已定义的外部变量。一般做法是外部变量的定义放在引用它的所有函数之前,这样可以避免在函数中多加一个 extern声明。
用 extern声明外部变量时,类型名可以写也可以省写。
例如上例中的,extern int A;”也可以写成:
,extern A;”。
2,在多文件的程序中声明外部变量一个C程序可以由一个或多个源程序文件组成。如果程序只由一个源文件组成,使用外部变量的方法前面已经介绍。如果程序由多个源程序文件组成,
那么在一个文件中想引用另一个文件中已定义的外部变量,有什么办法呢?如果一个程序包含两个文件,在两个文件中都要用到同一个外部淞縉 um,
不能分别在两个文件中各自定义一个外部变量 Num,
否则在进行程序的连接时会出现“重复定义”的错误。
正确的做法是:在任一个文件中定义外部变量 Num,
而在另一文件中用 extern对 Num作“外部变量声明”。 即 extern Num;在编译和连接时,系统会由此知道 Num是一个已在别处定义的外部变量,并将在另一文件中定义的外部变量的作用域扩展到本文件,在本文件中可以合法地引用外部变量
Num。下面举一个简单的例子来说明这种引用。
例 8.21用 extern将外部变量的作用域扩展到其他文件。
本程序的作用是给定 b的值,输入 a和 m,求 a× b和
am的值。
文件 file1,c中的内容为:
int A; /*定义外部变量 */
main()
{ int power( int); /*对调用函数作声明 */
int b= 3,c,d,m;
printf( "enter the number a and its power m:
\ n");
scanf( "% d,% d",& A,& m);
c= A*b;
printf( "% d**% d=% d\ n",A,b,c);
d= power( m);
printf( "% d*% d=% d",A,m,d);
}
文件 file2,c中的内容为:
extern A; /*声明 A为一个已定义的外部变量 */
power( int n) ;
{ int i,y= 1;
for( i= 1; i<= n; i++)
y*= A;
return( y);
}
可以看到,file2,c文件中的开头有一个 extern声明,
它声明在本文件中出现的变量 A是一个已经在其他文件中定义过的外部变量,本文件不必再次为它分配内存。本来外部变量 A的作用域是 file1.c,但现在用 extern声明将其作用域扩大到 file2.c文件。假如程序有 5个源文件,在一个文件中定义外部整型变量 A,其他 4个文件都可以引用 A,但必须在每一个文件中都加上一个 extern A;声明。在各文件经过编译后,将各目标文件联接成一个可执行的目标文件。
但是用这样的全局变量应十分慎重,因为在执行一个文件中的函数时,可能会改变了该全局变量的值,
它会影响到另一文件中的函数执行结果。
有的读者可能会问,extern既可以用来扩展外部变量在本文件中的作用域,又可以使外部变量的作用域从一个文件扩展到程序中的其他文件,那么系统怎么区别处理呢?实际上,在编译时遇到 extern时,
先在本文件中找外部变量的定义,如果找到,就在本文件中扩展作用域。如果找不到,就在连接时从其他文件中找外部变量的定义,如果找到,
就将作用域扩展到本文件。如果找不到,按出错处理。
8.9.6 用 static声明外部变量有时在程序设计中希望某些外部变量只限于被本文件引用,而不能被其他文件引用。这时可以在定义外部变量时加一个 static声明。例如:
file1.cfile2.c
static int A;extern int A;
main ( )fun (int n)
{ {…
…A=A n;
}
…
}
在 file1,c中定义了一个全局变量 A,但它用 static声明,因此只能用于本文件,虽然在 file2,c文件中用了 extern int A;,但 file2,c文件中用了
extern int A;,但 file2,c文件中无法使用 file1,c中的全局变量 A。这种加上 static声明、只能用于本文件的外部变量(全局变量)称为静态外部变量,在程序设计中,常由若干人分别完成各个模块,各人可以独立地在其设计的文件中使用相同的外部变量名而互不相干。只需在每个文件中的外部变量前加上 static即可。这就为程序的模块化、通用性提供方便。如果已知道其他文件不引用本文件的外部变量,可以对本文件中的外部变量都加上 static,成为静态外部变量,以免被其他文件误用。
需要指出对外部变量加 static声明,并不意味着这时才是静态存储(存放在静态存储区中),而不加 static的是动态存储(存放在动态存储区)。
两种形式的外部变量都是静态存储方式,只是作用范围不同而已,都是在编译时分配内存的。
需要指出对外部变量加 static声明,并不意味着这时才是静态存储(存放在静态存储区中),而不加
static的是动态存储(存放在动态存储区)。两种形式的外部变量都是静态存储方式,只是作用范围不同而已,都是在编译时分配内存的。
8.9.7 关于变量的声明和定义在第 2章中我们介绍了如何定义一个变量。在本章中又介绍了如何对一个变量的存储类别作声明。可能有些读者弄不清楚定义与声明有什么区别,它们是否一回事。在 C语言中,
关于定义与声明这两个名词的使用上始终存在着混淆。不仅许多初学者没有搞清楚,连不少介绍 C语言的教材和书刊也没有给出准确的介绍。从第 2章已经知道,一个函数一般由两部分组成,(1)声明部分; (2)执行语句。声明部分的作用是对有关的标识符(如变量、函数、结构体、共用体等)的属性进行说明。对于函数,声明和定义的区别是明显的,在本章 8.4.3节中己说明,函数的声明是函数的原型,
而函数的定义是函数的本身。
对函数的声明是放在声明部分中的,而函数的定义显然不在声明部分的范围内,它是一个独立的模块。对变量而言,声明与定义的关系稍微复杂一些。在声明部分出现的变量有两种情况:一种是需要建立存储空间的 (如,int a; ):另一种是不需要建立存储空间的
(如,extern a;)。前者称为“定义性声明” (defining declaration),或称定义( definition),
或称定义( definition)。 后者称为“引用性声明” (referenceing declaration)。广义地说,声明包括定义,但并非所有的声明都是定义。对,int a;” 而言,
它既是声明,又是定义。而对,extern a;” 而言,它是声明而不是定义。一般为了叙述方便,把建立存储空间的声明称定义,而把不需要建立存储空间的声明称为声明。显然这里指的声明是狭义的,即非定义性声明。例如:
main()
{extern A; /*是声明不是定义。声明 A是一个已定义的外部变量 */
…
}
int A; /*是定义,定义 A为整型外部变量 */
外部变量定义和外部变量声明的含义是不同的。外部变量的定义只能有一次,它的位置在所有函数之外,而同一文件中的外部变量的声明可以有多次,它的位置可以在函数之内(哪个函数要用就在哪个函数中声明)也可以在函数之外(在外部变量的定义点之前)。
系统根据外部变量的定义(而不是根据外部变量的声明)
分配存储单元。对外部变量的初始化只能在“定义”
时进行,而不能在“声明”中进行。所谓“声明”,
其作用是声明该变量是一个已在后面定义过的外部变量,仅仅是为了“提前”引用该变量而作的“声明”。
extern只用作声明,而不用于定义。
用 static来声明一个变量的作用有二,(1) 对局部变量用 static声明,则为该变量分配的空间在整个程序执行期间始终存在。 (2) 全局变量用 static声明,则该变量的作用域只限于本文件模块 (即被声明的文件中)。
请注意,用 auto,register,static声明变量时,是在定义变量的基础上加上这些关键字,而不能单独使用。下面用法不对:
int a;/*定义整型变量 a*/
static a;/*对变量 a声明为静态变量 */
编译时会被认为“重新定义”。
8.9.8 存储类别小结从上可知,对一个数据的定义,需要指定两种属性:
数据类型和存储类别,分别使用两个关键字。如:
static int a;(静态内部整型变量或静态外部整型变量)
auto char c; (自动变量,在函数内定义)
register int d;(寄存器变量,在函数内定义)
此外,可以用 extern声明变量为已定义的外部变量,
如:
extern b; (声明 b是一个已被定义的外部变量)
下面从不同角度做些归纳:
(1) 从作用域角度分,有局部变量和全局变量。它们采用的存储类别如下:
局部变量自动变量,即动态局部变量 (离开函数,值就消失 )
静态局部变量 (离开函数,值仍保留 )
寄存器变量 (离开函数,值就消失 )
(形式参数可以定义为自动变量或寄存器变量 )
全局变量静态外部变量 (只限本文件引用 )
外部变量 (即非静态的外部变量,允许其他文件引用 )
(2) 从变量存在的时间来区分,有动态存储和静态存储两种类型。静态存储是程序整个运行时间都存在,而动态存储则是在调用函数时临时分配单元。
动态存储自动变量 (本函数内有效 )
寄存器变量 (本函数内有效 )
形式参数静态存储静态局部变量 (函数内有效 )
静态外部变量 (本文件内有效 )
外部变量 (其他文件可引用 )
图 8.19
(3) 从变量值存放的位置来区分,可分为,
内存中静态存储区静态局部变量
静态外部变量 (函数外部静态变量 )
外部变量 (可为其他文件引用 )
内存中动态存储区,自动变量和形式参数
CPU中的寄存器,寄存器变量
(4) 关于作用域和生存期的概念。从前面叙述可以知道,
对一个变量的性质可以从两个方面分析,一是从变量的作用域,一是从变量值存在时间的长短,即生存期。前者是从空间的角度,后者是从时间的角度。
二者有联系但不是同一回事。图 8.19是作用域的示意图,图 8.20是生存期的示意图。
如果一个变量在某个文件或函数范围内是有效的,则称该文件或函数为该变量的作用域,在此作用域内可以引用该变量,所以又称变量在此作用域内“可见”,这种性质又称为变量的“可见性”,例如图
8.19中变量 a,b在函数 f1中“可见”。如果一个变量值在某一时刻是存在的,则认为这一时刻属于该变量的“生存期”,或称该变量在此时刻“存在”。
图 8.20
(5) static对局部变量和全局变量的作用不同。对局部变量来说,它使变量由动态存储方式改变为静态存储方式。而对全局变量来说,它使变量局部化 (局部于本文件 ),但仍为静态存储方式 \.从作用域角度看,
凡有 static声明的,其作用域都是局限的,或者是局限于本函数内 (静态局部变量 ),或者局限于本文件内
(静态外部变量 )。
8.10 内部函数和外部函数函数本质上是全局的,因为一个函数要被另外的函数调用,但是,也可以指定函数不能被其他文件调用。
根据函数能否被其他源文件调用,将函数区分为内部函数和外部函数。
8.10.1 内部函数如果一个函数只能被本文件中其他函数所调用,它称为内部函数。在定义内部函数时,在函数名和函数类型的前面加 static。即 static 类型标识符函数名
(形参表 )如
static int fun(int a,int b)
内部函数又称静态函数。使用内部函数,可以使函数只局限于所在文件,如果在不同的文件中有同名的内部函数,互不干扰。这样不同的人可以分别编写不同的函数,而不必担心所用函数是否会与其他文件中函数同名,通常把只能由同一文件使用的函数和外部变量放在一个文件中,在它们前面都谝詓 tatic
使之局部化,其他文件不能引用。
8.10.2外部函数
(1) 在定义函数时,如果在函数首部的最左端冠以关键字 extern,则表示此函数是外部函数,可供其他文件调用。
如函数首部可以写为
extern int fun (int a,int b)
这样,函数 fun就可以为其他文件调用。 C语言规定,
如果在定义函数时省略 extern,则隐含为外部函数。
本书前面所用的函数都是外部函数。
(2) 在需要调用此函数的文件中,用 extern声明所用的函数是外部函数。
例 8.22
有一个字符串,内有若干个字符,今输入一个字符,要求程序将字符串中该字符删去。用外部函数实现。
file1,c(文件 1)
main()
{ extern enter-string( char str[ 80]);
extern delete-strin g( char str[ ],char ch);
extern print-string(char str[]);
3行声明在本函数中将要调用的在其他文件中定义的 3个函数 */
char c;
char str[ 80];
enter-string( str);
scanf( "% c",& c);
delete-string( str,c);
print-string( str);
}
file2,c(文件 2)
# include <studio,h>
enter-string( char str[ 80
函数 enter-string*/
{ gets( str);} str*/
file3,c(文件 3)
delete-string(char str[],char ch)
义外部函数 delete-string /
{ int i,j;
for( i=j= 0; str[ i]!= ′\ 0′; i++)
if( str[ i]!= ch)
str[j++]= str[ i];
str[ j]= ′\ 0′;
}
file4,c(文件 4)
print-string( char str
print-string /
{
printf( "% s",str);
}
运行情况如下:
abcdefgc (输入 str)
c (输入要删去的字符)
abdefg (输出已删去指定字符的字符串)
整个程序由 4个文件组成。每个文件包含一个函数。
主函数是主控函数,除声明部分外,由 4个函数调用语句组成。其中 scanf是库函数,另外 3个是用户自己定义的函数。函数 delete-string的作用是根据给定的字符串 str和要删除的字符 ch,对 str作删除处理。算法是这样的:对 str数组的字符逐个检查,
如果不是被删除的字符就将它存放在数组中,见图
8.21(设删除空格)。从 str[ 0]开始逐个检查数组元素值是否等于指定要删除的字符,若不是就留在数组中,若是就不保留。从图中可以看到,应该使 str[ 0]赋给 str[ 0],str[ 1 str[ 1],str
[ 2 str[ 2],str[ 3 str[ 3],然后 str[ 5]
str[ 4] …… 请读者注意分析如何控制 i和j的变化,以便使被删除的字符不保留在原数组中。这个题目当然可以设两个数组,把不删除的字符一一赋给新数组。但我们只用一个数组,只把不被删除的字符保留下来。由于 i总是大于或等于j,因此最后保留下来的字符不会覆盖未被检测处理的字符。
最后将结束符‘\ 0’也复制到被保留的字符后面。
图 8.21
程序中 3个函数都定义为外部函数。在 main函数中用
extern声明在 main函数中用到的 enter-string、
delete-string,print-string是在其他文件中定义的外部函数。通过此例可知:使用 extern声明就能够在一个文件中调用其他文件中定义的函数,或者说把该函数的作用域扩展到本文件。 extern声明的形式就是在函数原型基础上加关键字 extern(见本例 main
函数中的声明形式)。由于函数在本质上是外部的,
在程序中经常要调用外部函数,为方便编程,C语言允许在声明函数时省写 extern。例 8.21程序 main
函数中对 power函数的声明就没有用 extern,但作用相同。一般都省写 extern,例如例 8.22程序 main
函数中的第一个函数声明可写成
enter-string(char str[ 80] );这就是我们多次用过的函数原型。由此可以进一步理解函数原型的作用。
用函数原型也能够把函数的作用域扩展到定义该函数的文件之外(不必使用 extern)。只要在使用该函数的每一个文件中包含该函数的函数原型即可。
函数原型通知编译系统:该函数在本文件中稍后定义,或在另一文件中定义。利用函数原型扩展函数作用域最常见的例子是 #include命令的应用。在前面几章中曾多次使用过 #include命令,并提到过:
在 #include命令所指定的“头文件”中包含有调用库函数时所需的信息。例如,在程序中需要调用 sin
函数,但三角函数并不是由用户在本文件中定义的,
而是存放在数学函数库中的。按以上的介绍,必须在本文件中写出 sin函数的原型,否则无法调用 sin
函数。 sin函数的原型是 double sin(double x);显然,
要求程序设计者在调用库函数时先从手册中查出所用的库函数的原型,并在程序中一一写出来是麻烦而困难的。为减少程序设计者的困难,在头文件
math.h中包括了所有数学函数的原型和其他有关信息,用户只需用以下 #include命令:
#include <math.h>
这样,在该文件中就能合法地调用各数学库函数了。
8.11 如何运行一个多文件的程序前面已提到一个程序往往由多个文件组成。那么如何把这些文件编译连接成一个统一的可执行文件并运行呢?
1,用 Turbo C集成环境如果要运行例 8.22程序,应该如何进行呢?它包含 4个文件:
(1) 先后输入并编辑 4个文件,并分别以文件名 file1.c、
file2.c,file3.c,file4.c存储在磁盘上。
(2) 在编译状态下,建立一个“项目文件”,它不包括任何程序语句,而只包括组成程序的所有的文件名。即
file1.c
file2.c
file3.c
file4.c
扩展名,c可以省写。 4个文件顺序任意,可以连续写在同一行上,如,file3 file1 file2 file4如果这些源文件不在当前目录下,应指出路径。
(3) 将以上内容存盘,文件名自定,但扩展名必须为,prj(表示为 project文件)。今设文件名为 a.prj。
在 Turbo C主菜单中选择 Project菜单,按回车键后出现下拉菜单,找到其中的 Project name项并按回车键,屏幕上会出现一个对话框,询问项目文件名。
|---------Project Name---------|
| *.prj |
| |
|-----------------------------------|
输入项目文件名 a.prj以代替 *.prj。此时子菜单中的
Project name后面会显示出项目文件名 a.prj,表示当前准备编译的是 a.prj中包括的文件。
(4) 按功能键 F9,进行编译连接,系统先后将 4个文件翻译成目标文件,并把它们连接或一个可执行文件 a.exe(文件名主干与项目文件相同)。
(5) 按 Ctrl+F9键,即可运行可执行文件 a.exe
2,在MS C上进行编译连接先分别对 4个文件进行编译,得到 4个,obj文件。然后用 link把 4个目标文件(,obj文件)连接起来。
可用以下命令,link file1+ file2+ file3+ file4得到一个可执行的文件 file1.exe,然后运行它。
3,用 #include命令将 file2,c,file3,c和 file4,c包含到 file1,c中。在
file1,c中的开头加 3行:
# include "file2,c"
# include "file3,c"
# include "file4,c"
这时,在编译时,系统自动将这 3个文件放到 main函数的前头,作为一个整体编译,而不是分 4个文件编译。这时,这些函数被认为是在同一文件中,不再是作为外部函数被其他文件调用了。 main函数中原有的 extern声明可以不要。
习题
8.1 写两个函数,分别求两个整数的最大公约数和最小公倍数,用主函数调用这两个函数,并输出结果,两个整数由键盘输入。
8.2 求方程 ax2+bx+c=0的根,用 3个函数分别求当 b2-
4ac大于 0、等于 0和小于 0时的根并输出结果。从主函数输入 a,b,c的值。
8.3 写一个判素数的函数,在主函数输入一个整数,输出是否素数的信息。
8.4 写一函数,使给定的一个二维数组 (3× 3)转置,即行列互换。
8.5 写一函数,使输入的一个字符串按反序存放,在主函数中输入和输出字符串。
8.6 写一函数,将两个字符串连接。
8.7 写一函数,输入一个 4位数字,要求输出这 4个数字字符,但每两个数字间空一个空格。如输入 1990,应输出,1 9 9 0”。
8.8 编写一函数,由实参传来一个字符串,统计此字符串中字母、数字、空格和其他字符的个数,在主函数中输入字符串以及输出上述的结果。
8.9 写一函数,输入一行字符,将此字符串中最长的单词输出。
8.10 写一函数,用“起泡法”对输入的 10个字符按由小到大顺序排列。
8.11 用弦截法求根。方程为 ax3+bx2+cx+d=0,系数 a、
b,c,d的值依次为 1,2,3,4,的值依次为 1,2,
3,4,由主函数输入。求 x在 1附近的一个实根。求出根后由主函数输出。
8.12 输入 10个学生 5门课的成绩,分别用函数求,① 每个学生平均分 ;② 每门课的平均分 ;③ 找出最高的分数所对应的学生和课程 ;④ 求平均分方差,σ=1n∑x2i-
(∑xi/n)2,xi为某一学生的平均分。
8.13 写几个函数,① 输入 10个职工的姓名和职工号,②
按职工号由小到大顺序排序,姓名顺序也随之调整 ;③ 要求输入一个职工号,用折半查找法找出该职工的姓名,从主函数输入要查找的职工号,输出该职工姓名。
8.14 写一函数,输入一个十六进制数,输出相应的十进制数。
8.15 给出年、月、日,计算该日是该年的第 n天。
8.2 函数定义的一般形式
8.3 函数参数和函数的值
8.4 函数的调用
8.5 函数的嵌套调用
8.6 函数的递归调用
8.7 数组作为函数参数
8.8 局部变量和全局变量
8.9 变量的存储类别
8.10 内部函数和外部函数
8.11 如何运行一个多文件的程序习题第 8章 函 数
8.1 概述一个较大的程序一般应分为若干个程序模块,每一个模块用来实现一个特定的功能。所有的高级语言中都有子程序这个概念,用子程序实现模块的功能。在C语言中,子程序的作用是由函数完成的。一个C程序可由一个主函数和若干个函数构成。由主函数调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多个函数调用任意多次。图 8.1是一个程序中函数调用的示意图。
在程序设计中,常将一些常用的功能模块编写成函数,放在函数库中供公共选用。要善于利用函数,
以减少重复编写程序段的工作量。
先举一个简单的函数调用的例子。例 8.1
main()
{ printstar();/ * 调用 printstar函数 */
print-message();/ * 调用 print message */
printstar(); / * 调用 printstar函数 */
}
printstar() / *printstar函数 */
{
printf(" * * * * * * * * * * * * * * * * * *\ n");
}
print-message()
/ * print-message函数 */
{
printf( "How do you do!\ n");
}
运行情况如下:
* * * * * * * * * * * * * * * * * *
How do you do!
* * * * * * * * * * * * * * * * * *
图 8.1
printstar和 print-message都是用户定义的函数名,分别用来输出一排,*”号和一行信息。
说明:
(1) 一个源程序文件由一个或多个函数组成。一个源程序文件是一个编译单位,即以源程序为单位进行编译,而不是以函数为单位进行编译。
(2) 一个C程序由一个或多个源程序文件组成。对较大的程序,一般不希望全放在一个文件中,而将函数和其他内容(如预定义)分别放在若干个源文件中,再由若干源文件组成一个 C程序。这样可以分别编写、分别编译,提高调度效率。一个源文件可以为多个 C程序公用。
(3) C程序的执行从 main函数开始,调用其他函数后流程回到 main函数,在 main函数中结束整个程序的运行。 main函数是系统定义的。
(4) 所有函数都是平行的,即在定义函数时是互相独立的,一个函数并不从属于另一函数,即函数不能嵌套定义(这是和 PASCAL不同的),函数间可以互相调用,但不能调用 main函数。
(5) 从用户使用的角度看,函数有两种:
① 标准函数,即库函数。这是由系统提供的,用户不必自己定义这些函数,可以直接使用它们。应该说明,不同的 C系统提供的库函数的数量和功能不同,
当然有一些基本的函数是共同的。
② 用户自己定义的函数。用以解决用户的专门需要。
(6) 从函数的形式看,函数分两类:
① 无参函数。如例 8.1中的 printstar和 print-message就是无参函数。在调用无参函数时,主调函数并不将数据传送给被调用函数,一般用来执行指定的一组操作
(例如,例 8.1那样),printstar函数的作用是输出 18
个星号。无参函数可以带回或不带回函数值,但一般以不带回函数值的居多。
② 有参函数。在调用函数时,在主调函数和被调用函数之间有数据传递。也就是说,主调函数可以将数据传给被调用函数使用,被调用函数中的数据也可以带回来供主调函数使用。
类型标识符 函数名()
{声明部分语句
}
例 8.1中的 printstar和 print-message函数都是无参函数。用,类型标识符,指定函数值的类型,即函数带回来的值的类型。无参函数一般不需要带回函数值,因此可以不写类型标识符,
例 8.1就如此。
8.2 函数定义的一般形式
1,无参函数的定义形式类型标识符函数名(形式参数表列)
{声明部分语句
}
例如:
int max( int x,int y)
{ int z;/ *
z= x> y? x∶ y;
return(z);
}
2,有参函数定义的一般形式这是一个求 x和 y二者中大者的函数,笫 1行第一个关键字 int表示函数值是整型的。 max为函数名。括号中有两个形式参数 x和 y,它们都是整型的。在调用此函数时,主调函数把实际参数的值传递给被调用函数中的形式参数 x和 y。花括弧内是函数体,它包括声明部分和语句部分。在声明部分定义所用的变量,此外对将要调用的函数作声明
(见 8.4.3节)。在函数体的语句中求出z的值
(为 x与 y中大者),return(z)的作用是将z的值作为函数值带回到主调函数中。 return后面的括弧中的值 (z)作为函数带回的值(或称函数返回值)。
在函数定义时已指定 max函数为整型,在函数体中定义z为整型,二者是一致的,将z作为函数 max
的值带回调用函数(见例 8.2)。
如果在定义函数时不指定函数类型,系统会隐含指定函数类型为 int型。因此上面定义的 max函数左端的
int可以省写。
3,可以有“空函数”
它的形式为类型说明符函数名( )
{ }
例如:
dummy(){}
调用此函数时,什么工作也不做,没有任何实际作用。
在主调函数中写上,dummy();” 表明,这里要调用一个函数”,而现在这个函数没有起作用,
等以后扩充函数功能时补充上。 在程序设计中往往根据需要确定若干模块,分别由一些函数来实现。 而在第一阶段只设计最基本的模块,其他一些次要功能或锦上添花的功能则在以后需要时陆续补上。在编写程序的开始阶段,可以在将来准备扩充功能的地方写上一个空函数(函数名取将来采用的实际函数名(如用 merge()、
matproduct(),oncatenate(),shell()等,
分别代表合并、矩阵相乘、字符串连接、希尔法排序等),只是这些函数未编好,先占一个位置,
以后用一个编好的函数代替它。这样做,程序的结构清楚,
可读性好,以后扩充新功能方便,对程序结构影响不大。空函数在程序设计中常常是有用的。
4,对形参的声明的传统方式在老版本 C语言中,对形参类型的声明是放在函数定义的笫 2行,也就是不在笫 1行的括号内指定形参的类型,而在括号外单独指定,例如上面定义的 max函数可以写成以下形式:
int max( x,y) /* 指定形参 x,y */
int x,y; /* 对形参指定类型 */
{ int z;
z = x > y? x,y;
return(z);
}
一般把这种方法称为传统的对形参的声明方式,而把前面介绍过的方法称为现代的方式。 Turbo C和目前使用的多数 C版本对这两种方法都允许使用,
两种用法等价,A NSI新标准推荐前一种方法,即现代方式。它与 PASCAL语言中所用的方法是类似的。本书中的程序采用新标准推荐的现代方式。
但由于有些过去写的书籍和程序使用传统方式,
因此读者应对它有所了解,以便能方便地阅读它们。
8.3.1 形式参数和实际参数在调用函数时,大多数情况下,主调函数和被调用函数之间有数据传递关系。这就是前面提到的有参函数。前面已提到:在定义函数时函数名后面括弧中的变量名称为“形式参数”(简称“形参”),在主调函数中调用一个函数时,函数名后面括弧中的参数 (可以是一个表达式 )称为“实际参数”(简称“实参”)。
例 8.2调用函数时的数据传递。
main()
{ int a,b,c;
8.3 函数参数和函数的值
scanf("% d,% d",& a,& b);
c= max( a,b);
printf("M ax is% d",c);
}
max( int x,int y max /
{
int z;
z= x> y? x∶ y;
return(z);
}
运行情况如下:
7,8
M ax is 8
程序中第 7~ 12行是一个函数定义(注意第 7行的末尾没有分号)。第 7行定义了一个函数名 max和指定两个形参 x,y及其类型。程序第 4行是一个调用函数语句,max后面括弧内的 a,b是实参。 a和 b是 main
函数中定义的变量,x和 y是函数 max中的形式参数。
通过函数调用,使两个函数中的数据发生联系。见图 8.2。
图 8.2
关于形参与实参的说明:
(1) 在定义函数中指定的形参,在未出现函数调用时,
它们并不占内存中的存储单元。只有在发生函数调用时,函数 max中的形参才被分配内存单元。在调用结束后,形参所占的内存单元也被释放。
(2) 实参可以是常量、变量或表达式,如:
max( 3,a+ b);
但要求它们有确定的值。在调用时将实参的值赋给形参(如果形参是数组名,则传递的是数组首地址而不是数组的值。请参阅 8.7节)。
(3) 在被定义的函数中,必须指定形参的类型(见例
8.2程序第 7行)。
(4) 实参与形参的类型应相同或赋值兼容。例 8.2中实参和形参都是整型,这是合法的、正确的。如果实参为整型而形参 x为实型,或者相反,则按第 2章介绍的不同类型数值的赋值规则进行转换。例如实参值 a为 3.5,而形参 x为整型,则将实数 3.5转换成整数 3,然后送到形参 b。但此时应将 max函数放在 main函数的前面或在 main函数中对被调用函数
max作原型声明,否则会出错。关于对函数的声明见 8.4.3小节。字符型与整型可以互相通用。
(5) C语言规定,实参变量对形参变量的数据传递都是“值传递”,即单向传递,只由实参传给形参,
而不能由形参传回来给实参,这是和FOR TRA
N不同的。在内存中,实参单元与形参单元是不同的单元。如图 8.3所示。
图 8.3 图 8.4
在调用函数时,给形参分配存储单元,并将实参对应的值传递给形参,调用结束后,形参单元被释放,
实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数的实参的值。例如,若在执行函数过程中
x和 y的值变为 10和 15,而 a和 b仍为 2和 3,见图 8.4。
8.3.2 函数的返回值通常,希望通过函数调用使主调函数能得到一个确定的值,这就是函数的返回值。例如,例 8.2中,
max( 2,3)的值是 3,max( 5,2)的值是 5。赋值语句将这个函数值赋给变量 c。下面对函数值作一些说明:
(1) 函数的返回值是通过函数中的 return语句获得的。
return语句将被调用函数中的一个确定值带回主调函数中去。见图 8.2中从 return语句返回的箭头。
如果需要从被调用函数带回一个函数值(供主调函数使用),被调用函数中必须包含 return语句。
如果不需要从被调用函数带回函数值可以不要
return语句。
一个函数中可以有一个以上的 return语句,执行到哪一个 return语句,哪一个语句起作用。
return语句后面的括弧也可以不要,如 return z;
它与,return(z);”等价。
return后面的值可以是一个表达式。例如,例 8.2中的函数 max可以改写如下:
max( int x,int y)
{
return( x> y? x∶ y);
}
这样的函数体更为简短,只用一个 return语句就把求值和返回都解决了。
(2) 函数值的类型。既然函数有返回值,这个值当然应属于某一个确定的类型,应当在定义函数时指定函数值的类型。例如:
int max( float x,float y) /* 函数值为整型 */
char letter( char c1,char c2) /* 函数值为字符型
*/
double min( int x,int y) /* 函数值为双精度型 */
读者会问:例 8.2中的函数定义并没有说明其类型,
为什么?C语言规定,凡不加类型说明的函数,一律自动按整型处理。例 8.2中的 max函数返回值为整型,因此可不必说明。
在定义函数时对函数值说明的类型一般应该和
return语句中的表达式类型一致。例如,例 8.2中用隐含方式指定 max函数值为整型,而变量z也被指定为整型,通过 return语句把z的值作为 max的函数值,由 max带回主调函数。z的类型与 max函数的类型是一致的,是正确的。
(3) 如果函数值的类型和 return语句中表达式的值不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换。即函数类型决定返回值的类型。
例 8.3返回值类型与函数类型不同。将例 8.2稍作改动
(注意是变量的类型改动)。
main()
{
float a,b;
int c;
scanf("% f,% f,",& a,& b);
c= max( a,b);
printf("M axis% d\ n",c);
}
max( float x,float y);
{ float z; /* z为实型变量 */
z= x> y? x∶ y;
return(z);
}
运行情况如下:
1,5,2,5
M ax is 2
函数 max定义为整型,而 return语句中的z为实型,
二者不一致,按上述规定,先将z转换为整型,然后 max( x,y)带回一个整型值 2回主调函数 main。
如果将 main函数中的 c定义为实型,用% f格式符输出,也是输出 2,000000。
有时,可以利用这一特点进行类型转换,如在函数中进行实型运算,希望返回的是整型量,可让系统去自动完成类型转换。但这种做法往往使程序不清晰,可读性降低,容易弄错,而且并不是所有的类型都能互相转换的(如实数与字符类型数据之间)。因此建议初学者不要采用这种方法,而应做到使函数类型与 return返回值的类型一致。
(4) 如果被调用函数中没有 return语句,并不带回一个确定的、用户所希望得到的函数值,但实际上,
函数并不是不带回值,而只是不带回有用的值,
带回的是一个不确定的值。例如,在例 8.1程序中,
尽管没有要求 printstar和 print-message函数带回值,但是如果在程序中出现下面的语句也是合法的:
{ int a,b,c;
a= printstar();
b= print-message();
c= printstar();
printf("% d,% d,% d\ n",a,b,c);
}
运行时除了得到和例 8.1一样的结果外,还可以输出
a,b,c的值(今为 21,20,21)。 a,b,c的值不一定有实际意义(今 printstar函数输出 21个字符,
返回值为 21,print-message输出 20个字符,返回值为 20)。
(5) 为了明确表示“不带回值”,可以用,void”定义
“无类型”(或称“空类型”)。例如,例 8.1中的定义可以改为
void printstar()
{ … }
void print-message()
{ … }
这样,系统就保证不使函数带回任何值,即禁止在调用函数中使用被调用函数的返回值。如果已将
printstar和 print-message函数定义为 void类型,则下面的用法就是错误的:
a= printstar();
b= print-message();
编译时会给出出错信息。
为使程序减少出错,保证正确调用,凡不要求带回函数值的函数,一般应定义为 void类型。许多C语言书的程序中都大量用到 void类型函数,读者应对此有一定了解。
8.4.1 函数调用的一般形式函数调用的一般形式为函数名(实参表列);如果是调用无参函数,则“实参表列”可以没有,但括弧不能省略,见例 8.1。如果实参表列包含多个实参,则各参数间用逗号隔开。实参与形参的个数应相等,类型应一致。实参与形参按顺序对应,
一一传递数据。但应说明,如果实参表列包括多个实参,对实参求值的顺序并不是确定的,有的系统按自左至右顺序求实参的值,有的系统则按自右至左顺序。许多C版本(例如 Turbo C和M
S C)是按自右而左的顺序求值。
8.4 函数的调用例 8.4 main()
{ int i= 2,p;
p= f( i,++ i); /* 函数调用 */
printf("% d",p);
}
int f( int a,int b) /* 函数定义 */
{ int c;
if( a> b) c= 1;
else if( a== b) c= 0;
else c=- 1;
return( c);
}
在 Turbo C系统上运行的结果为,0
如果按自左至右顺序求实参的值,则函数调用相当于 f( 2,3),程序运行应得结果为“- 1”。若按自右至左顺序求实参的值,则它相当于 f( 3,3),
程序运行结果为,0”。读者可以在所用的计算机系统上试一下,以便知道它所处理的方法。由于存在上述情况,使程序通用性受到影响。因此应当避免这种容易引起不同理解的情况。如果本意是按自左而右顺序求实参的值,可以改写为
j= i;
k=++ i;
p= f(j,k);
如果本意是自右而左求实参的值,可改写为
j=++ i;
p= f( j,j);
这种情况在 printf函数中也同样存在,如
printf("% d,% d",i,i++);
也发生上述同样的问题,若 i的原值为 3,在 Turbo C
上运行结果为 4,3。请读者务必注意,应该避免这种容易混淆的用法。
8.4.2 函数调用的方式按函数在程序中出现的位置来分,可以有以下三种函数调用方式:
1,函数语句把函数调用作为一个语句。如例 8.1中的 printstar
();这时不要求函数带回值,只要求函数完成一定的操作。
2,函数表达式函数出现在一个表达式中,这种表达式称为函数表达式。这时要求函数带回一个确定的值以参加表达式的运算。例如,c= 2*max( a,b);函数 max
是表达式的一部分,它的值乘 2再赋给 c。
3,函数参数函数调用作为一个函数的实参。例如,m= max( a,
max( b,c));其中 max( b,c)是一次函数调用,它的值作为 max另一次调用的实参。 m的值是
a,b,c三者最大的。又如,printf ("%d",max
(a,b));也是把 max( a,b)作为 printf函数的一个参数。
函数调用作为函数的参数,实质上也是函数表达式形式调用的一种,因为函数的参数本来就要求是表达式形式。
8.4.3 对被调用函数的声明和函数原型在一个函数中调用另一函数(即被调用函数)需要具备哪些条件呢?
(1) 首先被调用的函数必须是已经存在的函数(是库函数或用户自己定义的函数)。但光有这一条件还不够。
(2) 如果使用库函数,一般还应该在本文件开头用#
include命令将调用有关库函数时所需用到的信息
“包含”到本文件中来。例如,前几章中已经用过的# include <studio,h>其中,studio,h”是一个“头文件”。在 studio,h文件中放了输入输出库函数所用到的一些宏定义信息。如果不包含
,studio,h”文件中的信息,就无法使用输入输出库中的函数。同样,使用数学库中的函数,应该用# include <math,h>.h是头文件所用的后缀,
标志头文件( header file)。有关宏定义等概念请见第 8章。
(3) 如果使用用户自己定义的函数,而且该函数与调用它的函数(即主调函数)在同一个文件中,一般还应该在主调函数中对被调用的函数作声明,
即向编译系统声明将要调用此函数,并将有关信息通知编译系统。“声明” 一词的原文是
declaration,过去在许多书中译为“说明”,近年来,愈来愈多的计算机专家提出应称为声明,
作者也认为称为“声明”更确切,表意更清楚。
例 8.5对被调用的函数作声明。
main()
{ float add( float x,float y);/ *对被调用函数的声明 *
/
float a,b,c;
scanf("% f,% f",& a,& b);
c= add( a,b);
printf(" sum is% f",c);
}
float add( float x,float y) /*函数首部 */
{ float z; /* 函数体 */
z= x+ y;
return(z);
}
运行情况如下:
3.6,6.5
sum is 10.000000
这是一个很简单的函数调用,函数 add的作用是求两个实数之和,得到的函数值也是实型。请注意程序第 2行,float add( float x,float y);是对被调用的 add函数作声明。注意:对函数的“定义”和
“声明”不是一回事。“定义”是指对函数功能的确立,包括指定函数名,函数值类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。而“声明” 的作用则是把函数的名字、
函数类型以及形参的类型、个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致)。
从程序中可以看到对函数的声明与函数定义中的第
1行(函数首部)基本上是相同的。因此可以简单地照写已定义的函数的首部,再加一个分号,就成为了对函数的“声明”。
其实,在函数声明中也可以不写形参名,而只写形参的类型。如,float add(float,float);在 C语言中,
把以上形式的函数声明称为函数原型 (function
prototype)。使用函数原型是 N C的一个重要特点。
它的作用主要是利用它在程序的编译阶段对调用函数的合法性进行全面检查。从例 8.5中可以看到
main函数的位置在 add函数的前面,而在进行如果没有对函数的声明,当编译到包含函数调用的语句,c=add(a,b)”;时,编译系统不知道 add是不是函数名,也无法判断实参( a和 b)的类型和个数是否正确,因而无法进行正确性的检查。只有在运行时才会发现实参与形参的类型或个数不一致,
出现运行错误。但是在运行阶段发现错误并重新调试程序,是比较麻烦的,工作量也较大。
应当在编译阶段尽可能多地发现错误,随之纠正错误。现在我们在函数调用之前用函数原型做了函数声明。因此编译系统记下了所需调用的函数的有关信息,在对,c=add(a,b)”;进行编译时就
“有章可循”了。编译系统根据函数的原型对函数的调用的编译时是从上到下逐行进行的,合法性进行全面的检查。和函数原型不匹配的函数调用会导致编译出错。 它属于语法错误。用户根据屏幕显示的出错信息很容易发现和纠正错误。
函数原型的一般形式为
(1) 函数类型 函数名 (参数类型 1,参数类型 2……)
(2) 函数类型 函数名 (参数类型 1,参数名 1,参数类型 2,参数名 2……)
第 (1)种形式是基本的形式。为了便于阅读程序,也允许在函数原型中加上参数名,就成了第 (2)种形式。但编译系统不检查参数名。因此参数名是什么都无所谓。上面程序中的声明也可以写成
float add(float a,float b);/* 参数名不用 x,y,而用 a,b */
效果完全相同。
应当保证函数原型与函数首部写法上的一致,即函数类型、函数名、参数个数、参数类型和参数顺序必须相同。函数调用时函数名、实参个数应与函数原型一致。实参类型必须与函数原型中的形参类型赋值兼容,按第 2章介绍的赋值规则进行类型转换。如果不是赋值兼容,就按出错处理。
说明:
(1) 以前的 C版本的函数声明方式不是采用函数原型,
而只声明函数名和函数类型。例如在例 8.5中也可以采用下面的函数声明形式
float add( );
不包括参数类型和参数个数。系统不检查参数类型和参数个数。新版本也兼容这种用法,但不提倡这种用法,因为它未进行全面的检查。
(2) 实际上,如果在函数调用之前,没有对函数作声明,则编译系统会把笫一次遇到的该函数形式
(函数定义或函数调用)作为函数的声明,并将函数类型默认为 int型。例如例 8.2在调用 max函数之前没有进行函数声明,编译时首先遇到的函数形式是函数调用,max(a,b)”,由于对原型的处理是不考虑参数名的,因此系统将 max()加上 int作为的函数声明,即
int max( );
因此,不少 C教材说,如果函数类型为整型,可以在函数调用前不必作函数声明。但是使用这种方法时,系统无法对参数的类型做检查。若调用函数时参数使用不当,在编译时也不会报错。因此,为了程序清晰和安全,建议都加以声明为好。例如在例 8.2中最好加上以下函数声明:
int max(int,int);
或
int max(int x,int y);
(3) 如果被调用函数的定义出现在主调函数之前,
可以不必加以声明。因为编译系统已经先知道了已定义的函数类型,会根据函数首部提供的信息对函数的调用作正确性检查。
如果把例 8.5改写如下(即把 main函数放在 add函数的下面),就不必在 main函数中对 add声明。
float add( float x,float y)
{ floatz;
z= x+ y;
return(z);
}
main()/ *不必对 add函数作声明 */
{
float a,b,c;
scanf("% f,% f",& a,& b);
c= add( a,b);
printf("% f",c);
}
( 4) 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必对所调用的函数再作声明 。例如:
char letter( char,char);
/ *以下 3行在所有函数之前,且在函数外部 */
float f( float,float);
int i( float,float);
main()
{ … } /*不必声明它所调用的函数 */
char letter( char c1,char c2) /*定义 letter函数 */
{ … }
float f( float x,float y) /*定义 f函数 */
{ … }
int i( float j,float k) /*定义 i函数 */
{ … }
除了以上 (2)(3)(4) 所提到的三种情况外,都应该按上述介绍的方法对所调用函数作声明,否则编译时就会出现错误。用函数原型来声明函数,还能减少编写程序时可能出现的错误。由于函数声明的位置与函数调用语句的位置比较近,因此在写程序时便于就近参照函数原型来书写函数调用,不易出错。
8.5 函数的嵌套调用
C语言的函数定义都是互相平行、独立的,也就是说在定义函数时,一个函数内不能包含另一个函数,这是和 PASCAL不同的( PASCAL允许在定义一个函数时,
其函数体内又包含另一个函数的完整定义,这就是嵌套定义。这个内嵌的函数只能被 包含它的函数所调用,
其他函数不能调用它)。
C语句不能嵌套定义函数,但可以嵌套调用函数,
也就是说,在调用一个函数的过程中,又调用另一个函数。见图 8.5。
图 8.5
图 8.5表示的是两层嵌套(连 main函数共 3层函数),
其执行过程是:
(1) 执行 main函数的开头部分;
(2) 遇函数调用 a的操作语句,流程转去 a函数;
(3) 执行 a函数的开头部分;
(4) 遇调用 b函数的操作语句,流程转去函数 b;
(5) 执行 b函数,如果再无其他嵌套的函数,则完成 b
函数的全部操作;
(6) 返回调用 b函数处,即返回 a函数;
(7) 继续执行 a函数中尚未执行的部分,直到 a函数结束;
(8) 返回 main函数中调用 a函数处;
(9) 继续执行 main函数的剩余部分直到结束。
例 8.6用弦截法求方程 x3-5x2+16x-80=0的根。
方法如下,
(1) 取两个不同点 x1,x2,如果 f(x1)和 f(x2)符号相反,则
(x1,x2)区间内必有一个根。如果 f(x1)与 f(x2)同符号,
则应改变 x1\,x2,直到 f(x1),f(x2)异号为止。注意 x1、
x2的值不应差太大,以保证 (x1,x2)区间内只有一个根。
(2) 连接 f(x1)和 f(x2)两点,此线 (即弦 )交 x轴于 x,见图
8.6。
x点坐标可用下式求出,
x=x1·f(x2)-x2·f(x1)f(x2)-f(x1)
再从 x求出 f(x)\.
(3) 若 f(x)与 f(x1)同符号,则根必在 (x,x2)区间内,此时将 x作为新的 x1。如果 f(x)与 f(x2)同符号,则表示根在 (x1,x)区间内,将 x作为新的 x2。
图 8.6
图 8.7
(4) 重复步骤 (2) 和 (3),直到 | f(x)|< ε 为止,ε为一个很小的数,例如 10-6,此时认为 f(x)≈0.根据上述思路画出 N-S流程图,见图 8.7。
分别用几个函数来实现各部分功能,
(1) 用函数 f(x)来求 x的函数,x3-5x2+16x-80\.
(2) 用函数 xpoint (x1,x2)来求 f(x1)和 f(x2)的连线与 x
轴的交点 x的坐标。
(3) 用函数 root (x1,x2)来求 (x1,x2)区间的那个实根。
显然,执行 root函数过程中要用到函数 xpoint,而执行 xpoint函数过程中要用到 f函数。
请读者先分析下面的程序。
#include <math.h>
float f(float x) / * 定义 f函数,以实现 f(x)= x3-
5x2+16x-80 */
{
float y;
y=(( x- 5,0) *x+ 16,0) *x- 80,0;
return( y);
}
float xpoint(float x1,float x2) / *定义 xpoint函数,
求出弦与 x轴交点 */
{
float y;
y=( x1*f( x2)- x2*f( x1))/( f( x2)
- f( x1));
return( y);
}
float root( float x1,float x2) /*定义 root函数,求近似根 */
{
int i;
float x,y,y1;
y1= f( x1);
do
{
x= xpoint( x1,x2);
y= f( x);
if( y*y1> 0) f( x)与 f( x1)同符号 */
{ y1= y;
x1= x;}
else
x2= x;
} while( fabs( y)>= 0,0001);
return( x);
}
main() /*主函数 */
{
float x1,x2,f1,f2,x;
do
{
printf(" input x1,x2:\ n");
scanf(“% f,% f”,& x1,& x2);
f1= f( x1);
f2= f( x2);
} while( f1*f2>= 0);
x= root( x1,x2);
printf("A root of equation is% 8,4f",x);
}
运行情况如下:
input x1,x2:
2,6
A root of equation is 5,0000
从程序可以看到:
( 1) 在定义函数时,函数名为 f,xpoint,root的 3个函数是互相独立的,并不互相从属。这 3个函数均定为实型。
( 2) 3个函数的定义均出现在 main函数之前,因此在
main函数中不必对这 3个函数作类型说明。
( 3) 程序从 main函数开始执行。先执行一个 do-while
循环,作用是:输入 x1和 x2,判别 f( x1)和 f( x2)
是否异号,如果不是异号则重新输入 x1和 x2,直到满足 f( x1)与 f( x2)异号为止。然后用函数调用 root
( x1,x2)求根 x。调用 root函数过程中,要调用
xpoint函数来求 f( x1)与 fx2)连线的交点 x。在调用
xpoint函数过程中要用到函数 f来求 x1和 x2的相应的函数值 f( x1)和 f( x2)。这就是函数的嵌套调用。见图 8.8。
图 8.8
(4) 在 root函数中要用到求绝对值的函数 fabs,它是对实型数求绝对值的标准函数。它属于数学函数库,故在文件开头用# include <math,h>即把使用数学库函数时所需用到的有关信息包含进来。
8.6 函数的递归调用在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。C语言的特点之一就在于允许函数的递归调用。例如:
int f( int x)
{ int y,z;
z= f( y);
return( 2*z);
}
在调用函数 f的过程中,又要调用 f函数,这是直接调用本函数,见图 8.9。下面是间接调用本函数。
图 8.9 图 8.10
在调用 f1函数过程中要调用 f2函数,而在调用 f2函数过程中又要调用 f1函数,见图 8.10。
从图上可以看到,这两种递归调用都是无终止的自身调用。显然,程序中不应出现这种无终止的递归调用,而只应出现有限次数的、有终止的递归调用,这可以用 if语句来控制,只有在某一条件成立时才继续执行递归调用,否则就不再继续。
关于递归的概念,有些初学者感到不好理解,下面用一个通俗的例子来说明。
例 8.7 有 5个人坐在一起,问第 5个人多少岁?他说比第 4个人大 2岁。问第 4个人岁数,他说比第 3个人大 2岁。问第 3个人,又说比第 2个人大 2岁。问第 2个人,说比第 1个人大 2岁。最后问第 1个人,
他说是 10岁。请问第 5个人多大。显然,这是一个递归问题。要求第 5个人的年龄,就必须先知道第
4个人的年龄,而第 4个人的年龄也不知道,要求第 4个人的年龄必须先知道第 3个人的年龄,而第 3
个人的年龄又取决于第 2个人的年龄,第 2个人的年龄取决于第 1个人的年龄。而且每一个人的年龄都比其前 1个人的年龄大 2。
即 age( 5)= age( 4)+ 2
age( 4)= age( 3)+ 2
age( 3)= age( 2)+ 2
age( 2)= age( 1)+ 2
age( 1)= 10
可以用式子表述如下:
age( n)= 10 ( n= 1)
age( n- 1)+ 2 ( n> 1)
可以看到,当 n> 1时,求第 n个人的年龄的公式是相同的。因此可以用一个函数表示上述关系。图 8.11
表示求第 5个人年龄的过程。
图 8.11
从图可知,求解可分成两个阶段:第一阶段是“回推”,即将第 n个人的年龄表示为第( n- 1)个人年龄的函数,而第( n- 1)个人的年龄仍然不知道,还要“回推”到第( n- 2)个人的年龄 ……
直到第 1个人年龄。此时 age( 1)已知,不必再向前推了。
然后开始第二阶段,采用递推方法,从第 1个人的已知年龄推算出第 2个人的年龄( 12岁),从第 2个人的年龄推算出第 3个人的年龄( 14岁) …… 一直推算出第 5个人的年龄( 18岁)为止。也就是说,
一个递归的问题可以分为“回推”和“递推”两个阶段。要经历许多步才能求出最后的值。显而易见,如果要求递归过程不是无限制进行下去,
必须具有一个结束递归过程的条件。例如,age
( 1)= 10,就是使递归结束的条件。
可以用一个函数来描述上述递归过程:
age( int n) /*求年龄的递归函数 */
{int c; /* c用作存放函数的返回值的变量 */
if( n== 1) c= 10;
else c= age( n- 1)+ 2;
return( c);
}
main()
{
printf( "% d",age( 5));
}
运行结果如下:
18
main函数中只有一个语句。整个问题的求解全靠一个 age( 5)函数调用来解决。函数调用过程如图
8.12所示。
图 8.12
从图 8.12可以看到,age函数共被调用 5次,即 age
( 5),age( 4),age( 3),age( 2),age
( 1)。其中 age( 5)是 main函数调用的,其余 4
次是在 age函数中调用的,即递归调用 4次。请读者仔细分析调用的过程。应当强调说明的是在某一次调用 age函数时并不是立即得到 age( n)的值,
而是一次又一次地进行递归调用,到 age( 1)时才有确定的值,然后再递推出 age( 2),age( 3)、
age( 4),age( 5)。请读者将程序和图 8.11、图
8.12结合起来认真分析。
例 8.8用递归方法求 n!。
求 n!可以用递推方法,即从 1开始,乘 2,再乘
3…… 一直乘到 n。这种方法容易理解,也容易实现。
递推法的特点是从一个已知的事实出发,按一定规律推出下一个事实,再从这个新的已知的事实出发,
再向下推出一个新的事实 …… 这是和递归不同的。
求 n! ··也可以用递归方法,即 5!等于 4! × 5,而 4!
= 3! × 4…1 != 1。可用下面的递归公式表示:
n!= 1( n= 0,1)
n·( n- 1)!( n> 1)
有了例 8.7的基础,很容易写出本题的程序:
float fac( int n)
{
float f;
if( n< 0) { printf( "n< 0,dataerror! "); f=-1; }
else if( n== 0‖ n== 1) f= 1;
else f= fac( n- 1) *n;
return( f);
}
main()
{
int n;
float y;
printf( "input a integer number,");
scanf( "% d",& n);
y= fac( n);
printf( "% d!=% 15,0f",n,y);
}
运行情况如下:
input a integer number,10
10!= 3628800.
例 8.9hanoi(汉诺)塔问题。这是一个古典的数学问题,是一个只有用递归方法(而不可能用其他方法)解决的问题。问题是这样的:古代有一个梵塔,塔内有 3个座 A,B,C,开始时A座上有 64个盘子,盘子大小不等,大的在下,小的在上(图
8.13)。有一个老和尚想把这 64个盘子从A座移到
C座,但每次只允许移动一个盘,且在移动过讨性?个座上都始终保持大盘在下,小盘在上。在移动过程中可以利用B座,要求编程序打印出移动的步骤。
图 8.13
可以肯定地说:任何一个人(包括“天才”) 都不可能直接写出移动盘子的每一个具体步骤。请读者试验一下按上面的规律将 5个盘子从 A座移到 C座,能否直接写出每一步骤?老和尚自然会这样想:假如有另外一个和尚能有办法将 63个盘子从一个座移到另一座。
那么,问题就解决了。此时老和尚只需这样做:
(1) 命令第 2个和尚将 63个盘子从 A座移到 B座;
(2) 自己将 1个盘子(最底下的、最大的盘子)从 A座移到 C座;
(3) 再命令第 2个和尚将 63个盘子从 B座移到 C座。
至此,全部任务完成了。这就是递归方法。但是,
有一个问题实际上未解决:第 2个和尚怎样才能将
63个盘子从 A座移到 B座?为了解决将 63个盘子从
A座移到 B座,第 2个和尚又想:如果有人能将 62个盘子从一个座移到另一座,我就能将 63个盘子从 A
座移到 B座,他是这样做的:
(1) 命令第 3个和尚将 62个盘子从 A座移到 C座;
(2) 自己将 1个盘子从 A座移到 B座;
(3) 再命令第 3个和尚将 62个盘子从 C座移到 B座。
再进行一次递归。如此“层层下放”,直到后来找到第 63个和尚,让他完成将 2个盘子从一个座移到另一座,进行到此,问题就接近解决了。最后找到第 64个和尚,让他完成将 1个盘子从一个座移到另一座,至此,全部工作都已落实,都是可以执行的。可以看出,递归的结束条件是最后一个和尚只需移一个盘子。否则递归还要继续进行下去。
应当说明,只有第 64个和尚的任务完成后,第 63个和尚的任务才能完成。只有第 2到第 64个和尚任务完成后,第 1个和尚的任务才能完成。这是一个典型的递归的问题。为使问题简化,我们先分析将
A座上 3个盘子移到C座上的过程:
(1) 将A座上 2个盘子移到B座上(借助C);
(2) 将A座上 1个盘子移到C座上;
(3) 将B座上 2个盘子移到C座上(借助A)。
其中第 2步可以直接实现。第 1步又可用递归方法分解为:
1,1将A上 1个盘子从A移到C;
1,2将A上 1个盘子从A移到B;
1,3将C上 1个盘子从C移到B。
第 3步可以分解为:
3,1将B上 1个盘子从B移到A上;
3,2将B上 1个盘子从B移到C上;
3,3将A上 1个盘子从A移到C上。
将以上综合起来,可得到移动 3个盘子的步骤为
A → C,A → B,C → B,A → C,B → A,B →
C,A → C。
共经历 7步。由此可推出:移动 n个盘子要经历 2n-1
步。如移 4个盘子经历 15步,移 5个盘子经历 31步,
移 64个盘子经历 264-1步。
由上面的分析可知:将 n个盘子从A座移到C座可以分解为以下 3个步骤:
(1) 将A上 n- 1个盘借助C座先移到B座上。
(2) 把A座上剩下的一个盘移到C座上。
(3) 将 n- 1个盘从B座借助于A座移到C座上。
上面第 1步和第 3步,都是把 n- 1个盘从一个座移到另一个座上,采取的办法是一样的,只是座的名字不同而已。为使之一般化,可以将第 1步和第 3
步表示为:
“将,one” 座上 n- 1个盘移到,two” 座 (借助,three”
座 )。只是在第①步和第③步中,one,two,three
和A、B、C的对应关系不同。对第①步,对应关系是 one——A,two——B,three——C。对第③步,是,one——B,two——C,three——
A。
因此,可以把上面 3个步骤分成两类操作:
(1) 将 n- 1个盘从一个座移到另一个座上( n> 1)。
这就是大和尚让小和尚做的工作,它是一个递归的过程,即和尚将任务层层下放,直到第 64个和尚为止。
(2) 将 1个盘子从一个座上移到另一座上。这是大和尚自己做的工作。
下面编写程序。分别用两个函数实现以上的两类操作,用 hanoi函数实现上面第 1类操作(即模拟小和尚的任务),用 move函数实现上面第 2类操作(模拟大和尚自己移盘),函数调用 hanoi( n,one,two,three)表示
“将 n个盘子从,one” 座移到,three” 座的过程 (借助,two”针” )。函数调用 move( x,y)表示将 1个盘子从 x 座移到 y 座的过程。 x和 y是代表A、B、
C座之一,根据每次不同情况分别取A、B、C代入。
程序如下:
void move( char x,char y)
{
printf( "% c-->% c\ n",x,y);
}
void hanoi( int n,char one,char two,char
three)
/*将 n个盘从 one座借助 two座,移到 three座 */
{
if( n== 1) move( one,three);
else
{
hanoi( n- 1,one,three,two);
move( one,three);
hanoi( n- 1,two,one,three);
}
}
main()
{
int m;
printf( "input the number of diskes,");
scanf( "% d",& m);
printf( "The step to moving % 3d diskes:\ n",
m);
hanoi( m,′A ′,′B ′,′C ′);
}
运行情况如下:
input the number of diskes,3
The step to moving 3 diskes:
A-->C
A-->B
C-->B
A-->C
B-->A
B-->C
A-->C
在本程序中 move函数并未真正移动盘子,而只是打印出移盘的方案(从哪一个座移到哪一个座)。
由于篇幅关系,不再对上述程序做过多解释,请读者仔细理解。
8.7 数组作为函数参数前面已经介绍了可以用变量作函数参数,此外,数组元素也可以作函数实参,其用法与变量相同。数组名也可以作实参和形参,传递的是整个数组。
1,数组元素作函数实参由于实参可以是表达式形式,数组元素可以是表达式的组成部分,因此数组元素当然可以作为函数的实参,与用变量作实参一样,是单向传递,即“值传送”方式。
例 8.10有两个数组 a,b,各有 10个元素,将它们对应地逐个相比(即 a[ 0]与 b[ 0]比,a[ 1]与 b[ 1]
比 …… )。如果 a数组中的元素大于 b数组中的相应元素的数目多于 b数组中元素大于 a数组中相应元素的数目 (例如,a[ i] >b[ i] 6次,b[ i]
>a[ i] 3次,其中 i每次为不同的值 ),则认为 a数组大于 b数组,并分别统计出两个数组相应元素大于、等于、小于的次数。
程序如下:
main()
{ int large(int x,int y); /*函数声明 */
int a[ 10],b[ 10],i,n= 0,m= 0,k= 0;
printf(,enter array a∶ \ n”);
for( i= 0; i< 10; i++)
scanf( "% d",& a[ i]);
printf( "\ n");
printf( "enter array b∶ \ n");
for( i= 0; i< 10; i++)
scanf( "% d",& b[ i]);
printf( "\ n");
for( i= 0; i< 10; i++)
{if( large( a[ i],b[ i])== 1) n= n+ 1;
else if( large( a[ i],b[ i])== 0) m= m+ 1;
else k= k+ 1; }
printf("a[i]>b[i]%d times\ na[i]=b[i]%d times
\ na[i]<b[i]%d times\ n",n,m,k);
if(n>k) printf("array a is larger than array b\
n");
else if (n<k) printf("array a is smaller than
array b\ n");
else printf("array a is equal to array
b\ n");
}
large( int x,int y)
{int flag;
if( x> y) flag= 1;
else if( x< y) flag=- 1;
else flag= 0;
return( flag);
}
运行情况如下:
enter array a:
1 3 5 7 9 8 6 4 2 0
enter array b∶
5 3 8 9-1-3 5 6 0 4
a[ i]> b[ i] 4 times
a[ i]= b[ i] 1 times
a[ i]< b[ i] 5 times
array a is smaller than array b
2,数组名可作函数参数可以用数组名作函数参数,此时实参与形参都应用数组名(或用指针变量,见第 9章)。
例 8.11有一个一维数组 score,内放 10个学生成绩,
求平均成绩。
程序如下:
float average( float array[ 10])
{ int i;
float aver,sum= array[ 0];
for( i= 1; i< 10; i++)
sum= sum+ array[ i];
aver= sum/ 10;
return( aver);
}
main()
{ float score[ 10],aver;
int i;
printf( "input 10 scores:\ n");
for( i= 0; i< 10; i++)
scanf( "% f",& score[ i]);
printf( "\ n");
aver= average( score);
printf( "average score is % 5,2f",aver);
}
运行情况如下:
input 10 scores:
100567898,576879967,57597
average score is 83,40
说明:
(1) 用数组名作函数参数,应该在主调函数和被调用函数分别定义数组,例中 array是形参数组名,
score是实参数组名,分别在其所在函数中定义,
不能只在一方定义。
(2) 实参数组与形参数组类型应一致(今都为 float
型),如不一致,结果将出错。
(3) 在被调用函数中声明了形参数组的大小为 10,但在实际上,指定其大小是不起任何作用的,因为
C编译对形参数组大小不做检查,只是将实参数组的首地址传给形参数组。因此,score[ n]和
array[ n]指的是同一单元。
(4) 形参数组也可以不指定大小,在定义数组时在数组名后面跟一个空的方括弧,为了在被调用函数中处理数组元素的需要,可以另设一个参数,传递数组元素的个数,例 8.11可以改写为例 8.12形式。
例 8.12
float average( float array[ ]
,int n)
{ int i;
float aver,sum= array[ 0];
for( i= 1; i< n; i++)
sum= sum+ array[ i];
aver= sum/ n;
return( aver);
}
main()
{ float score-1[ 5]={ 98,5,97,91,5,60,
55};
float score-2[ 10] ={67.5,89.5,99,69.5,77,
89.5,76.5,54,60,99.5};
printf("the average of class A is %6.2f\ n",
average(score-1,5));
printf("the average of class B is %6.2f\ n",
average(score-2,10));
}
运行结果如下:
the average of class A is 80,40
the average of class B is 78,20
可以看出,两次调用 average函数时需要处理的数组元素个数是不同的,在第一次调用时用一个实参 5
传递给形参 n,表示求前面 5个学生的平均分。第 2次调用时,求 10个学生平均分。
(5) 最后应当说明一点,用数组名作函数实参时,不是把数组的值传递给形参,而是把实参数组的起始地址传递给形参数组,这样两个数组就共占同一段内存单元。见图 8.14。假若 a的起始地址为 1000,则 b数组的起始地址也是 1000,显然,a和 b同占一段内存单元,a[ 0]与 b[ 0]同占一个单元 …… 。由此可以看到,形参数组中各元素的值如发生变化会使实参数组元素的值同时发生变化,从图 8.14看是很容易理解的。这一点是与变量做函数参数的情况不相同的,务请注意。在程序设计中可以有意识地利用这一特点改变实参数组元素的值 (如排序 )。
关于数组名作为函数参数,将在第 9章介绍完指针变量后作进一步的说明。
起始地址 1000
a[ 0] a[ 1] a[ 2]
a[ 3] a[ 4] a[ 5] a[ 6] a[ 7] a[ 8] a[ 9]
2468101214161820
b[ 0] b[ 1] b[ 2] b[ 3] b[ 4] b[ 5] b[ 6] b
[ 7] b[ 8] b[ 9]
图 8.14
例 8.13
用选择法对数组中 10个整数按由小到大排序。所谓选择法就是先将 10个数中最小的数与 a[ 0]对换 ;再将
a[ 1]到 a[ 9]中最小的数与 a[ 1]对换 …… 每比较一轮,找出一个未经排序的数中最小的一个。共比较 9轮。
下面以 5个数为例说明选择法的步骤。
a[ 0] a[ 1] a[ 2] a[ 3] a[ 4]
3 6 1 9 4
未排序时的情况
1 6 3 9 4 将 5个数中最小的数 1与 a[ 0]
对换
1 3 6 9 4 将余下的 4个数中最小的数 3与 a
[ 1]对换
1 3 4 9 6 将余下的 3个数中最小的数 4与 a
[ 2]对换
1 3 4 6 9 将余下的 2个数中最小的数 6与 a
[ 3]对换,至此完成排序根据此思路编写程序如下:
void sort( int array[],int n)
{ int i,j,k,t;
for( i= 0; i< n- 1; i++)
{ k= i;
for(j= i+ 1;j< n;j++)
if( array[j]< array[ k]) k
=j;
t= array[ k]; array[ k]= array
[ i]; arra
y[ i]= t;}
}
main()
{ int a[ 10],i;
printf( "enter the array\ n");
for( i= 0; i< 10; i++)
scanf( "% d",& a[ i]);
sort( a,10);
printf( "the sorted array∶ \ n");
for( i= 0; i< 10; i++)
printf( "% d",a[ i]);
printf( "\ n");
}
可以看到在执行函数调用语句 sort( a,10);之前和之后,a数组中各元素的值是不同的。原来是无序的,执行 sort( a,10);后,a数组已经排好序了,这是由于形参数组 array已用选择法进行排序了,形参数组改变也使实参数组随之改变。
3,用多维数组名作函数参数多维数组元素可以作为实参,这点与前述相同。
可以用多维数组名作为实参和形参,在被调用函数中对形参数组定义时可以指定每一维的大小,也可以省略第一维的大小说明。如 int array[ 3]
[ 10];或 int array[ ][ 10];二者都合法而且等价。但是不能把第二维以及其他高维的大小说明省略。如下面是不合法的:
int array[ ][ ];
因为从实参传送来的是数组起始地址,在内存中各元素是一行接一行地顺序存放的,而并不区分行和列,
如果在形参中不说明列数,则系统无法决定应为多少行多少列。不能只指定第一维而省略第二维,下面写法是错误的:
int array[ 3][];
形参数组第一维的大小可以是任意的。例如,实参数组定义为 int score[ 5][ 10];而形参数组定义为
int array[ 3][ 10];均可以,C编译不检查第一维的大小。请读者从“传递地址”这一特点出发来思考这个问题。
例 8.14有一个 3× 4的矩阵,求所有元素中的最大值。
解此题的算法是:先使变量 max的初值为矩阵中第一个元素的值,然后将矩阵中各个元素的值与 max相比,每次比较后都把“大者”存放在 max中,全部元素比较完后,max 的值就是所有元素的最大值。
程序如下:
max-value( int array[ ][ 4])
{ int i,j,max;
max= array[ 0][ 0];
for( i= 0; i< 3; i++)
for(j= 0;j< 4;j++)
if( array[ i][j]> max) max= array[ i]
[ j];
return( max);
}
main()
{ int a[ 3][ 4]={{ 1,3,5,7},{ 2,4,
6,8},{ 15,17,34,12}};
printf( "max value is% d\ n",max-value
( a));
}
运行结果如下:
max value is 34
8.8.1 局部变量在一个函数内部定义的变量是内部变量,它只在本函数范围内有效,也就是说只有在本函数内才能使用它们,在此函数以外是不能使用这些变量的。
这称为“局部变量”。如:
float f1( int a)/*函数 f1*/
{int b,c;
… a,b,c有效
}
char f2(int x,int y)/*函数 f2*/
8.8 局部变量和全局变量
{int i,j;
} x,y,i,j有效
main( )/*主函数 */
{int m,n;
… m,n有效
}
说明,
(1) 主函数 main中定义的变量 (m,n)也只在主函数中有效,而不因为在主函数中定义而在整个文件或程序中有效,这是和 PASCAL不同的。主函数也不能使用其他函数中定义的变量。
(2) 不同函数中可以使用相同名字的变量,它们代表不同的对象,互不干扰。例如,在 f1函数中定义了变量
b,c,倘若在 f2函数中也定义变量 b和 c,它们在内存中占不同的单元,互不混淆。
(3) 形式参数也是局部变量。例如 f1函数中的形参 a,
也只在 f1函数中有效。其他函数不能调用。
(4) 在一个函数内部,可以在复合语句中定义变量,这些变量只在本复合语句中有效,这种复合语句也可称为“分程序”或“程序块”。
main ( )
{int a,b;
…
{int c;
c=a+b; c在此范围内有效 a,b在此范围内有效
…
}
…
}
变量 c只在复合语句 (分程序 )内有效,离开该复合语句该变量就无效,释放内存单元。
8.8.2 全局变量前已介绍,程序的编译单位是源程序文件,一个源文件可以包含一个或若干个函数 \.在函数内定义的变量是局部变量,而在函数之外定义的变量称为外部变量,外部变量是全局变量 (也称全程变量 )。全局变量可以为本文件中其他函数所共用。它的有效范围为从定义变量的位置开始到本源文件结束。如,
int p=1,q=5;/ /
float f1(a)/ 定义函数 f1 /
int a;
{int b,c;
…
}
char c1,c2;/ / 全局变量 p,q
char f2 (int x,int y)/*定义函数 f2/ 的作用范围
{int i,j; 全局变量 c1,
… c2 的作用范围
}
main ( )/ /
{int m,n;
…
}
p,q,c1,c2都是全局变量,但它们的作用范围不同,在 main函数和 f2函数中可以使用全局变量 p、
q,c1,c2,但在函数 f1中只能使用全局变量 p,q,
而不能使用 c1和 c2。
在一个函数中既可以使用本函数中的局部变量,又可以使用有效的全局变量。打个通俗的比方:国家有统一的法律和法令,各省还可以根据需要制定地方的法律、法令。在甲省,国家统一的法律法令和甲省的法律法令都是有效的,而在乙省,
则国家统一的和乙省的法律法令有效。显然,甲省的法律法令在乙省无效。
说明:
(1) 设全局变量的作用是增加了函数间数据联系的渠道。由于同一文件中的所有函数都能引用全局变量的值,因此如果在一个函数中改变了全局变量的值,就能影响到其他函数,相当于各个函数间有直接的传递通道。由于函数的调用只能带回一个返回值,因此有时可以利用全局变量增加与函数联系的渠道,从函数得到一个以上的返回值。
为了便于区别全局变量和局部变量,在 C程序设计人员中有一个不成文的约定(但非规定),将全局变量名的笫一个字母用大写表示。
例 8.15有一个一维数组,内放 10个学生成绩,写一个函数,求出平均分,最高分和最低分。
显然希望从函数得到 3个结果值,除了可得到一个函数的返回值以外,还可以利用全局变量。
float Max= 0,Min= 0
float average( float array[ ],int n) / * 定义函数,形参为数组 */
{ int i;
float aver,sum= array[ 0];
Max= Min= array[ 0];
for( i= 1; i< n; i++)
{ if( array[ i]> Max) Max= array[ i];
else if( array[ i]< Min) Min= array[ i];
sum= sum+ array[ i];
}
aver= sum/ n;
return( aver);
}
main()
{ float ave,score[ 10];
int i;
for( i= 0; i< 10; i++)
scanf( "% f",& score[ i]);
ave= average( score,10);
printf(“max=%6.2f \ nmin=%6.2f\
naverage=%6.2f\ n”,Max,Min,ave);
}
图 8.15
运行情况如下:
99 45 78 97 100 67,5 89 92 66 43
max= 100,00
min= 43,00
average= 77,65
函数 average中与外界有联系的变量与外界的联系如图
8.15所示。可以看出形参 array和 n的值由 main函数传递给形参,函数 average中 aver的值通过 return语句带回
main函数。 Max和 Min是全局变量,是公用的它的值可以供各函数使用,如果在一个函数中,改变了它们的值,在其他函数中也可以使用这个已改变的值。由此看出,可以利用全局变量以减少函数实参与形参的个数,从而减少内存空间以及传递数据时的时间消耗。
(2) 建议不在必要时不要使用全局变量,因为:
① 全局变量在程序的全部执行过程中都占用存储单元,
而不是仅在需要时才开辟单元。
② 它使函数的通用性降低了,因为函数在执行时要依赖于其所在的外部变量。如果将一个函数移到另一个文件中,还要将有关的外部变量及其值一起移过去。但若该外部变量与其他文件的变量同名时,就会出现问题,降低了程序的可靠性和通用性。在程序设计中,
在划分模块时要求模块的“内聚性”强、与其他模块的“耦合性”弱。即模块的功能要单一(不要把许多互不相干的功能放到一个模块中),与其他模块的相互影响要尽量少,而用全局变量是不符合这个原则的。
一般要求把C程序中的函数做成一个封闭体,除了可以通过“实参 ——形参”的渠道与外界发生联系外,
没有其他渠道。这样的程序移植性好,可读性强。
③ 使用全局变量过多,会降低程序的清晰性,人们往往难以清楚地判断出每个瞬时各个外部变量的值。在各个函数执行时都可能改变外部变量的值,
程序容易出错。因此,要限制使用全局变量。
(3) 如果在同一个源文件中,外部变量与局部变量同名,则在局部变量的作用范围内,外部变量被
“屏蔽”,即它不起作用。如:
例 8.16外部变量与局部变量同名。
int a=3,b=5;/* a,b */a,b作用范围
max (int a,int b)/ a,b /
{int c;
c=a> b?a∶ b; 形参 a,b作用范围
return (c);
}
main( )
{int a=8; / a /局部变量 a作用范围
printf (“%d”,max (a,b)); 全局变量 b的作用范围
}
运行结果为
8
我们故意重复使用 a,b作变量名,请读者区别不同的 a,b的含义和作用范围。第 1行定义了外部变量
a,b,并使之初始化。第 2行开始定义函数 max,
a和 b是形参,形参也是局部变量。函数 max中的 a、
b不是外部变量 a,b,它们的值是由实参传给形参的,外部变量 a,b在 max函数范围内不起作用。
最后 4行是 main函数,它定义了一个局部变量 a,
因此全局变量 a在 main函数范围内不起作用,而全局变量 b在此范围内有效。因此 printf函数中的
max( a,b)相当于 max( 8,5),程序运行后得到结果为 8。
8.9.1 动态存储方式与静态存储方式上一节已介绍了,从变量的作用域(即从空间)
角度来分,可以分为全局变量和局部变量。
可以从另一个角度,从变量值存在的时间(即生存期)角度来分,可以分为静态存储方式和动态存储方式。
所谓静态存储方式是指在程序运行期间分配固定的存储空间的方式。而动态存储方式则是在程序运行期间根据需要进行动态的分配存储空间的方式。
8.9 变量的存储类别用户区程序区静态存储区动态存储区图 8.16
先看一下内存中的供用户使用的存储空间的情况。
这个存储空间可以分为三部分,见图 8.16。
1,程序区
2,静态存储区
3,动态存储区数据分别存放在静态存储区和动态存储区中。全局变量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储区,程序执行完毕就释放。
在程序执行过程中它们占据固定的存储单元,而不是动态地进行分配和释放。
在动态存储区中存放以下数据:
①函数形式参数。在调用函数时给形参分配存储空间。
②自动变量(未加 static声明的局部变量,详见后面的介绍)。
③函数调用时的现场保护和返回地址等。对以上这些数据,在函数调用开始时分配动态存储空间,
函数结束时释放这些空间。在程序执行过程中,
这种分配和释放是动态的,如果在一个程序中两次调用同一函数,分配给此函数中局部变量的存储空间地址可能是不相同的。如果一个程序包含若干个函数,每个函数中的局部变量的生存期并不等于整个程序的执行周期,它只是程序执行周期的一部分。根据函数调用的需要,动态地分配和释放存储空间。
在C语言中每一个变量和函数有两个属性:数据类型和数据的存储类别。数据类型,读者已熟悉
(如整型、字符型等)。存储类别指的是数据在内存中存储的方法。存储方法分为两大类:静态存储类和动态存储类。具体包含四种:自动的
( auto),静态的( static),寄存器的
( register),外部的( xtern)。根据变量的存储类别,可以知道变量的作用域和生存期。
下面分别作介绍。
8.9.2 auto变量函数中的局部变量,如不专门声明为 static存储类别,
都是动态地分配存储空间的,数据存储在动态存储区中。函数中的形参和在函数中定义的变量 (包括在复合语句中定义的变量),都属此类,在调用该函数时系统会给它们分配存储空间,在函数调用结束时就自动释放这些存储空间。因此这类局部变量称为自动变量。自动变量用关键字 auto
作存储类别的声明。例如:
int f( int a) /*定义 f函数,a为形参 */
{ auto int b,c= 3; /*定义 b,c为自动变量 */
…
}
a是形参,b,c是自动变量,对 c赋初值 3。执行完 f
函数后,自动释放 a,b,c 所占的存储单元。
实际上,关键字,auto”可以省略,auto不写则隐含确定为“自动存储类别”,它属于动态存储方式。
程序中大多数变量属于自动变量。我们前面介绍的函数中定义的变量都没有声明为 auto,其实都隐含指定为自动变量。例如,在函数体中:
auto int b,c= 3;
int b,c= 3;
二者等价
8.9.3 用 static声明局部变量有时希望函数中的局部变量的值在函数调用结束后不消失而保留原值,即其占用的存储单元不释放,
在下一次该函数调用时,该变量已有值,就是上一次函数调用结束时的值。这时就应该指定该局部变量为“静态局部变量”,用关键字 static进行声明。通过下面简单的例子可以了解它的特点。
例 8.17考察静态局部变量的值。
f( int a)
{ auto b= 0;
static c= 3;
b= b+ 1;
c= c+ 1;
return( a+ b+ c);
}
main()
{ int a= 2,i;
for( i= 0; i< 3; i+
+)
printf( "% d ",f
( a));
}
运行结果为:
7 8 9
图 8.17
在第 1次调用 f函数时,b的初值为 0,c的初值为 3,
第 1次调用结束时,b= 1,c= 4,a+ b+ c= 7。由于 c是静态局部变量,在函数调用结束后,它并不释放,仍保留 c= 4。在第 2次调用 f函数时,b的初值为 0,而 c的初值为 4(上次调用结束时的值)。
见图 8.17。
对静态局部变量的说明:
(1) 静态局部变量属于静态存储类别,在静态存储区内分配存储单元。在程序整个运行期间都不释放。
而自动变量(即动态局部变量)属于动态存储类别,占动态存储区空间而不占静态存储区空间,
函数调用结束后即释放。
(2) 对静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的值。而对自动变量赋初值,不是在编译时进行的,而是在函数调用时进行,,每调用一次函数重新给一次初值,相当于执行一次赋值语句。
(3) 如在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动赋初值 0(对数值型变量)
或空字符(对字符变量)。而对自动变量来说,
如果不赋初值则它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另分配存储单元,而所分配的单元中的值是不确定的。
(4) 虽然静态局部变量在函数调用结束后仍然存在,
但其他函数是不能引用它的。
在什么情况下需要用局部静态变量呢?
(1) 需要保留函数上一次调用结束时的值。例如可以用下面方法求 n!。
例 8.18打印 1到 5的阶乘值。
int fac( int n)
{ static int f= 1;
f= f*n;
return( f);
}
main()
{ int i;
for( i= 1; i<= 5; i++)
printf( "% d!=% d\ n",i,fac( i));
}
运行结果为:
1!= 1
2!= 2
3!= 6
4!= 24
5!= 120
每次调用 fac( i),打印出一个 i!,同时保留这个 i!
的值以便下次再乘( i+ 1)。
(2) 如果初始化后,变量只被引用而不改变其值,则这时用静态局部变量比较方便,以免每次调用时重新赋值。
但是应该看到,用静态存储要多占内存(长期占用不释放,而不能像动态存储那样一个存储单元可供多个变量使用,节约内存),而且降低了程序的可读性,当调用次数多时往往弄不清静态局部变量的当前值是什么。因此,如不必要,不要多用静态局部变量。
8.9.4 register变量一般情况下,变量(包括静态存储方式和动态存储方式)的值是存放在内存中的。当程序中用到哪一个变量的值时,由控制器发出指令将内存中该变量的值送到运算器中。 经过运算器进行运算,如果需要存数,再从运算器将数据送到内存存放。见图
8.18。
图 8.18
如果有一些变量使用频繁(例如在一个函数中执行
10000次循环,每次循环中都要引用某局部变量),
则为存取变量的值要花不少时间。为提高执行效率,
C语言允许将局部变量的值放在 CPU中的寄存器中,
需要用时直接从寄存器取出参加运算,不必再到内存中去存取。由于对寄存器的存取速度远高于对内存的存取速度,因此这样做可以提高执行效率。这种变量叫做“寄存器变量”,用关键字 register作声明。例如,例 8.19是输出 1到 5的阶乘的值。
例 8.19使用寄存器变量。
int fac( int n)
{ register int i,f= 1;/ *定义寄存器变量 */
for( i= 1; i<= n; i++)
f= f*i;
return( f);
}
main()
{ int i;
for( i= 1; i<= 5; i++)
printf( "% d!=% d\ n",i,fac( i));
}
定义局部变量 f和 i是寄存器变量,如果 n的值大,则能节约许多执行时间。
说明:
(1) 只有局部自动变量和形式参数可以作为寄存器变量,其他(如全局变量)不行。在调用一个函数时占用一些寄存器以存放寄存器变量的值,函数调用结束释放寄存器。此后,在调用另一个函数时又可以利用它来存放该函数的寄存器变量。
(2) 一个计算机系统中的寄存器数目是有限的,不能定义任意多个寄存器变量。不同的系统允许使用的寄存器是不同的,而且对 register变量的处理方法也是不同的,有的系统对 register变量当作自动变量处理,分配内存单元,并不真正把它们存放在寄存器中,有的系统只允许将 int,char和指针型变量定义为寄存器变量。
当今的优化编译系统能够识别使用频繁的变量,从而自动地将这些变量放在寄存器中,而不需要程序设计者指定。因此在实际上用 register声明变量是不必要的。读者对它有一定了解即可。
(3) 局部静态变量不能定义为寄存器变量。不能写成
register static int a,b,c;不能把变量 a,b,c既放在静态存储区中,又放在寄存器中,二者只能居其一。对一个变量只能声明为一个存储类别。
8.9.5 用 extern声明外部变量外部变量(即全局变量)是在函数的外部定义的,
它的作用域为从变量的定义处开始,到本程序文件的末尾。在此作用域内,全局变量可以为程序中各个函数所引用。编译时将外部变量分配在静态存储区。有时需要用 extern来声明外部变量,以扩展外部变量的作用城。
1,在一个文件内声明外部变量如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件终了。如果在定义点之前的函数想引用该外部变量,则应该在引用之前用关键字 extern对该变量作“外部变量声明”。表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。例如:
例 8.20用 extern声明外部变量,扩展程序文件中的作用域。
int max( int x,int y max
{ int z;
z= x> y? x∶ y;
return(z);
}
main()
{ extern A,B; /*外部变量声明 */
printf( "% d",max( A,B));
}
int A= 13,B=- 8;
/
运行结果如下,13
在本程序文件的最后 1行定义了外部变量 A,B,但由于外部变量定义的位置在函数 main之后,因此本来在 main函数中不能引用外部变量 A和 B。现在我们在 main函数的第 2行用 extern对 A和 B进行“外部变量声明”,表示 A和 B是已经定义的外部变量
(但定义的位置在后面)。这样在 main函数中就可以合法地使用全局变量 A和 B了。如果不作
extern声明,编译时出错,系统不会认为 A,B是已定义的外部变量。一般做法是外部变量的定义放在引用它的所有函数之前,这样可以避免在函数中多加一个 extern声明。
用 extern声明外部变量时,类型名可以写也可以省写。
例如上例中的,extern int A;”也可以写成:
,extern A;”。
2,在多文件的程序中声明外部变量一个C程序可以由一个或多个源程序文件组成。如果程序只由一个源文件组成,使用外部变量的方法前面已经介绍。如果程序由多个源程序文件组成,
那么在一个文件中想引用另一个文件中已定义的外部变量,有什么办法呢?如果一个程序包含两个文件,在两个文件中都要用到同一个外部淞縉 um,
不能分别在两个文件中各自定义一个外部变量 Num,
否则在进行程序的连接时会出现“重复定义”的错误。
正确的做法是:在任一个文件中定义外部变量 Num,
而在另一文件中用 extern对 Num作“外部变量声明”。 即 extern Num;在编译和连接时,系统会由此知道 Num是一个已在别处定义的外部变量,并将在另一文件中定义的外部变量的作用域扩展到本文件,在本文件中可以合法地引用外部变量
Num。下面举一个简单的例子来说明这种引用。
例 8.21用 extern将外部变量的作用域扩展到其他文件。
本程序的作用是给定 b的值,输入 a和 m,求 a× b和
am的值。
文件 file1,c中的内容为:
int A; /*定义外部变量 */
main()
{ int power( int); /*对调用函数作声明 */
int b= 3,c,d,m;
printf( "enter the number a and its power m:
\ n");
scanf( "% d,% d",& A,& m);
c= A*b;
printf( "% d**% d=% d\ n",A,b,c);
d= power( m);
printf( "% d*% d=% d",A,m,d);
}
文件 file2,c中的内容为:
extern A; /*声明 A为一个已定义的外部变量 */
power( int n) ;
{ int i,y= 1;
for( i= 1; i<= n; i++)
y*= A;
return( y);
}
可以看到,file2,c文件中的开头有一个 extern声明,
它声明在本文件中出现的变量 A是一个已经在其他文件中定义过的外部变量,本文件不必再次为它分配内存。本来外部变量 A的作用域是 file1.c,但现在用 extern声明将其作用域扩大到 file2.c文件。假如程序有 5个源文件,在一个文件中定义外部整型变量 A,其他 4个文件都可以引用 A,但必须在每一个文件中都加上一个 extern A;声明。在各文件经过编译后,将各目标文件联接成一个可执行的目标文件。
但是用这样的全局变量应十分慎重,因为在执行一个文件中的函数时,可能会改变了该全局变量的值,
它会影响到另一文件中的函数执行结果。
有的读者可能会问,extern既可以用来扩展外部变量在本文件中的作用域,又可以使外部变量的作用域从一个文件扩展到程序中的其他文件,那么系统怎么区别处理呢?实际上,在编译时遇到 extern时,
先在本文件中找外部变量的定义,如果找到,就在本文件中扩展作用域。如果找不到,就在连接时从其他文件中找外部变量的定义,如果找到,
就将作用域扩展到本文件。如果找不到,按出错处理。
8.9.6 用 static声明外部变量有时在程序设计中希望某些外部变量只限于被本文件引用,而不能被其他文件引用。这时可以在定义外部变量时加一个 static声明。例如:
file1.cfile2.c
static int A;extern int A;
main ( )fun (int n)
{ {…
…A=A n;
}
…
}
在 file1,c中定义了一个全局变量 A,但它用 static声明,因此只能用于本文件,虽然在 file2,c文件中用了 extern int A;,但 file2,c文件中用了
extern int A;,但 file2,c文件中无法使用 file1,c中的全局变量 A。这种加上 static声明、只能用于本文件的外部变量(全局变量)称为静态外部变量,在程序设计中,常由若干人分别完成各个模块,各人可以独立地在其设计的文件中使用相同的外部变量名而互不相干。只需在每个文件中的外部变量前加上 static即可。这就为程序的模块化、通用性提供方便。如果已知道其他文件不引用本文件的外部变量,可以对本文件中的外部变量都加上 static,成为静态外部变量,以免被其他文件误用。
需要指出对外部变量加 static声明,并不意味着这时才是静态存储(存放在静态存储区中),而不加 static的是动态存储(存放在动态存储区)。
两种形式的外部变量都是静态存储方式,只是作用范围不同而已,都是在编译时分配内存的。
需要指出对外部变量加 static声明,并不意味着这时才是静态存储(存放在静态存储区中),而不加
static的是动态存储(存放在动态存储区)。两种形式的外部变量都是静态存储方式,只是作用范围不同而已,都是在编译时分配内存的。
8.9.7 关于变量的声明和定义在第 2章中我们介绍了如何定义一个变量。在本章中又介绍了如何对一个变量的存储类别作声明。可能有些读者弄不清楚定义与声明有什么区别,它们是否一回事。在 C语言中,
关于定义与声明这两个名词的使用上始终存在着混淆。不仅许多初学者没有搞清楚,连不少介绍 C语言的教材和书刊也没有给出准确的介绍。从第 2章已经知道,一个函数一般由两部分组成,(1)声明部分; (2)执行语句。声明部分的作用是对有关的标识符(如变量、函数、结构体、共用体等)的属性进行说明。对于函数,声明和定义的区别是明显的,在本章 8.4.3节中己说明,函数的声明是函数的原型,
而函数的定义是函数的本身。
对函数的声明是放在声明部分中的,而函数的定义显然不在声明部分的范围内,它是一个独立的模块。对变量而言,声明与定义的关系稍微复杂一些。在声明部分出现的变量有两种情况:一种是需要建立存储空间的 (如,int a; ):另一种是不需要建立存储空间的
(如,extern a;)。前者称为“定义性声明” (defining declaration),或称定义( definition),
或称定义( definition)。 后者称为“引用性声明” (referenceing declaration)。广义地说,声明包括定义,但并非所有的声明都是定义。对,int a;” 而言,
它既是声明,又是定义。而对,extern a;” 而言,它是声明而不是定义。一般为了叙述方便,把建立存储空间的声明称定义,而把不需要建立存储空间的声明称为声明。显然这里指的声明是狭义的,即非定义性声明。例如:
main()
{extern A; /*是声明不是定义。声明 A是一个已定义的外部变量 */
…
}
int A; /*是定义,定义 A为整型外部变量 */
外部变量定义和外部变量声明的含义是不同的。外部变量的定义只能有一次,它的位置在所有函数之外,而同一文件中的外部变量的声明可以有多次,它的位置可以在函数之内(哪个函数要用就在哪个函数中声明)也可以在函数之外(在外部变量的定义点之前)。
系统根据外部变量的定义(而不是根据外部变量的声明)
分配存储单元。对外部变量的初始化只能在“定义”
时进行,而不能在“声明”中进行。所谓“声明”,
其作用是声明该变量是一个已在后面定义过的外部变量,仅仅是为了“提前”引用该变量而作的“声明”。
extern只用作声明,而不用于定义。
用 static来声明一个变量的作用有二,(1) 对局部变量用 static声明,则为该变量分配的空间在整个程序执行期间始终存在。 (2) 全局变量用 static声明,则该变量的作用域只限于本文件模块 (即被声明的文件中)。
请注意,用 auto,register,static声明变量时,是在定义变量的基础上加上这些关键字,而不能单独使用。下面用法不对:
int a;/*定义整型变量 a*/
static a;/*对变量 a声明为静态变量 */
编译时会被认为“重新定义”。
8.9.8 存储类别小结从上可知,对一个数据的定义,需要指定两种属性:
数据类型和存储类别,分别使用两个关键字。如:
static int a;(静态内部整型变量或静态外部整型变量)
auto char c; (自动变量,在函数内定义)
register int d;(寄存器变量,在函数内定义)
此外,可以用 extern声明变量为已定义的外部变量,
如:
extern b; (声明 b是一个已被定义的外部变量)
下面从不同角度做些归纳:
(1) 从作用域角度分,有局部变量和全局变量。它们采用的存储类别如下:
局部变量自动变量,即动态局部变量 (离开函数,值就消失 )
静态局部变量 (离开函数,值仍保留 )
寄存器变量 (离开函数,值就消失 )
(形式参数可以定义为自动变量或寄存器变量 )
全局变量静态外部变量 (只限本文件引用 )
外部变量 (即非静态的外部变量,允许其他文件引用 )
(2) 从变量存在的时间来区分,有动态存储和静态存储两种类型。静态存储是程序整个运行时间都存在,而动态存储则是在调用函数时临时分配单元。
动态存储自动变量 (本函数内有效 )
寄存器变量 (本函数内有效 )
形式参数静态存储静态局部变量 (函数内有效 )
静态外部变量 (本文件内有效 )
外部变量 (其他文件可引用 )
图 8.19
(3) 从变量值存放的位置来区分,可分为,
内存中静态存储区静态局部变量
静态外部变量 (函数外部静态变量 )
外部变量 (可为其他文件引用 )
内存中动态存储区,自动变量和形式参数
CPU中的寄存器,寄存器变量
(4) 关于作用域和生存期的概念。从前面叙述可以知道,
对一个变量的性质可以从两个方面分析,一是从变量的作用域,一是从变量值存在时间的长短,即生存期。前者是从空间的角度,后者是从时间的角度。
二者有联系但不是同一回事。图 8.19是作用域的示意图,图 8.20是生存期的示意图。
如果一个变量在某个文件或函数范围内是有效的,则称该文件或函数为该变量的作用域,在此作用域内可以引用该变量,所以又称变量在此作用域内“可见”,这种性质又称为变量的“可见性”,例如图
8.19中变量 a,b在函数 f1中“可见”。如果一个变量值在某一时刻是存在的,则认为这一时刻属于该变量的“生存期”,或称该变量在此时刻“存在”。
图 8.20
(5) static对局部变量和全局变量的作用不同。对局部变量来说,它使变量由动态存储方式改变为静态存储方式。而对全局变量来说,它使变量局部化 (局部于本文件 ),但仍为静态存储方式 \.从作用域角度看,
凡有 static声明的,其作用域都是局限的,或者是局限于本函数内 (静态局部变量 ),或者局限于本文件内
(静态外部变量 )。
8.10 内部函数和外部函数函数本质上是全局的,因为一个函数要被另外的函数调用,但是,也可以指定函数不能被其他文件调用。
根据函数能否被其他源文件调用,将函数区分为内部函数和外部函数。
8.10.1 内部函数如果一个函数只能被本文件中其他函数所调用,它称为内部函数。在定义内部函数时,在函数名和函数类型的前面加 static。即 static 类型标识符函数名
(形参表 )如
static int fun(int a,int b)
内部函数又称静态函数。使用内部函数,可以使函数只局限于所在文件,如果在不同的文件中有同名的内部函数,互不干扰。这样不同的人可以分别编写不同的函数,而不必担心所用函数是否会与其他文件中函数同名,通常把只能由同一文件使用的函数和外部变量放在一个文件中,在它们前面都谝詓 tatic
使之局部化,其他文件不能引用。
8.10.2外部函数
(1) 在定义函数时,如果在函数首部的最左端冠以关键字 extern,则表示此函数是外部函数,可供其他文件调用。
如函数首部可以写为
extern int fun (int a,int b)
这样,函数 fun就可以为其他文件调用。 C语言规定,
如果在定义函数时省略 extern,则隐含为外部函数。
本书前面所用的函数都是外部函数。
(2) 在需要调用此函数的文件中,用 extern声明所用的函数是外部函数。
例 8.22
有一个字符串,内有若干个字符,今输入一个字符,要求程序将字符串中该字符删去。用外部函数实现。
file1,c(文件 1)
main()
{ extern enter-string( char str[ 80]);
extern delete-strin g( char str[ ],char ch);
extern print-string(char str[]);
3行声明在本函数中将要调用的在其他文件中定义的 3个函数 */
char c;
char str[ 80];
enter-string( str);
scanf( "% c",& c);
delete-string( str,c);
print-string( str);
}
file2,c(文件 2)
# include <studio,h>
enter-string( char str[ 80
函数 enter-string*/
{ gets( str);} str*/
file3,c(文件 3)
delete-string(char str[],char ch)
义外部函数 delete-string /
{ int i,j;
for( i=j= 0; str[ i]!= ′\ 0′; i++)
if( str[ i]!= ch)
str[j++]= str[ i];
str[ j]= ′\ 0′;
}
file4,c(文件 4)
print-string( char str
print-string /
{
printf( "% s",str);
}
运行情况如下:
abcdefgc (输入 str)
c (输入要删去的字符)
abdefg (输出已删去指定字符的字符串)
整个程序由 4个文件组成。每个文件包含一个函数。
主函数是主控函数,除声明部分外,由 4个函数调用语句组成。其中 scanf是库函数,另外 3个是用户自己定义的函数。函数 delete-string的作用是根据给定的字符串 str和要删除的字符 ch,对 str作删除处理。算法是这样的:对 str数组的字符逐个检查,
如果不是被删除的字符就将它存放在数组中,见图
8.21(设删除空格)。从 str[ 0]开始逐个检查数组元素值是否等于指定要删除的字符,若不是就留在数组中,若是就不保留。从图中可以看到,应该使 str[ 0]赋给 str[ 0],str[ 1 str[ 1],str
[ 2 str[ 2],str[ 3 str[ 3],然后 str[ 5]
str[ 4] …… 请读者注意分析如何控制 i和j的变化,以便使被删除的字符不保留在原数组中。这个题目当然可以设两个数组,把不删除的字符一一赋给新数组。但我们只用一个数组,只把不被删除的字符保留下来。由于 i总是大于或等于j,因此最后保留下来的字符不会覆盖未被检测处理的字符。
最后将结束符‘\ 0’也复制到被保留的字符后面。
图 8.21
程序中 3个函数都定义为外部函数。在 main函数中用
extern声明在 main函数中用到的 enter-string、
delete-string,print-string是在其他文件中定义的外部函数。通过此例可知:使用 extern声明就能够在一个文件中调用其他文件中定义的函数,或者说把该函数的作用域扩展到本文件。 extern声明的形式就是在函数原型基础上加关键字 extern(见本例 main
函数中的声明形式)。由于函数在本质上是外部的,
在程序中经常要调用外部函数,为方便编程,C语言允许在声明函数时省写 extern。例 8.21程序 main
函数中对 power函数的声明就没有用 extern,但作用相同。一般都省写 extern,例如例 8.22程序 main
函数中的第一个函数声明可写成
enter-string(char str[ 80] );这就是我们多次用过的函数原型。由此可以进一步理解函数原型的作用。
用函数原型也能够把函数的作用域扩展到定义该函数的文件之外(不必使用 extern)。只要在使用该函数的每一个文件中包含该函数的函数原型即可。
函数原型通知编译系统:该函数在本文件中稍后定义,或在另一文件中定义。利用函数原型扩展函数作用域最常见的例子是 #include命令的应用。在前面几章中曾多次使用过 #include命令,并提到过:
在 #include命令所指定的“头文件”中包含有调用库函数时所需的信息。例如,在程序中需要调用 sin
函数,但三角函数并不是由用户在本文件中定义的,
而是存放在数学函数库中的。按以上的介绍,必须在本文件中写出 sin函数的原型,否则无法调用 sin
函数。 sin函数的原型是 double sin(double x);显然,
要求程序设计者在调用库函数时先从手册中查出所用的库函数的原型,并在程序中一一写出来是麻烦而困难的。为减少程序设计者的困难,在头文件
math.h中包括了所有数学函数的原型和其他有关信息,用户只需用以下 #include命令:
#include <math.h>
这样,在该文件中就能合法地调用各数学库函数了。
8.11 如何运行一个多文件的程序前面已提到一个程序往往由多个文件组成。那么如何把这些文件编译连接成一个统一的可执行文件并运行呢?
1,用 Turbo C集成环境如果要运行例 8.22程序,应该如何进行呢?它包含 4个文件:
(1) 先后输入并编辑 4个文件,并分别以文件名 file1.c、
file2.c,file3.c,file4.c存储在磁盘上。
(2) 在编译状态下,建立一个“项目文件”,它不包括任何程序语句,而只包括组成程序的所有的文件名。即
file1.c
file2.c
file3.c
file4.c
扩展名,c可以省写。 4个文件顺序任意,可以连续写在同一行上,如,file3 file1 file2 file4如果这些源文件不在当前目录下,应指出路径。
(3) 将以上内容存盘,文件名自定,但扩展名必须为,prj(表示为 project文件)。今设文件名为 a.prj。
在 Turbo C主菜单中选择 Project菜单,按回车键后出现下拉菜单,找到其中的 Project name项并按回车键,屏幕上会出现一个对话框,询问项目文件名。
|---------Project Name---------|
| *.prj |
| |
|-----------------------------------|
输入项目文件名 a.prj以代替 *.prj。此时子菜单中的
Project name后面会显示出项目文件名 a.prj,表示当前准备编译的是 a.prj中包括的文件。
(4) 按功能键 F9,进行编译连接,系统先后将 4个文件翻译成目标文件,并把它们连接或一个可执行文件 a.exe(文件名主干与项目文件相同)。
(5) 按 Ctrl+F9键,即可运行可执行文件 a.exe
2,在MS C上进行编译连接先分别对 4个文件进行编译,得到 4个,obj文件。然后用 link把 4个目标文件(,obj文件)连接起来。
可用以下命令,link file1+ file2+ file3+ file4得到一个可执行的文件 file1.exe,然后运行它。
3,用 #include命令将 file2,c,file3,c和 file4,c包含到 file1,c中。在
file1,c中的开头加 3行:
# include "file2,c"
# include "file3,c"
# include "file4,c"
这时,在编译时,系统自动将这 3个文件放到 main函数的前头,作为一个整体编译,而不是分 4个文件编译。这时,这些函数被认为是在同一文件中,不再是作为外部函数被其他文件调用了。 main函数中原有的 extern声明可以不要。
习题
8.1 写两个函数,分别求两个整数的最大公约数和最小公倍数,用主函数调用这两个函数,并输出结果,两个整数由键盘输入。
8.2 求方程 ax2+bx+c=0的根,用 3个函数分别求当 b2-
4ac大于 0、等于 0和小于 0时的根并输出结果。从主函数输入 a,b,c的值。
8.3 写一个判素数的函数,在主函数输入一个整数,输出是否素数的信息。
8.4 写一函数,使给定的一个二维数组 (3× 3)转置,即行列互换。
8.5 写一函数,使输入的一个字符串按反序存放,在主函数中输入和输出字符串。
8.6 写一函数,将两个字符串连接。
8.7 写一函数,输入一个 4位数字,要求输出这 4个数字字符,但每两个数字间空一个空格。如输入 1990,应输出,1 9 9 0”。
8.8 编写一函数,由实参传来一个字符串,统计此字符串中字母、数字、空格和其他字符的个数,在主函数中输入字符串以及输出上述的结果。
8.9 写一函数,输入一行字符,将此字符串中最长的单词输出。
8.10 写一函数,用“起泡法”对输入的 10个字符按由小到大顺序排列。
8.11 用弦截法求根。方程为 ax3+bx2+cx+d=0,系数 a、
b,c,d的值依次为 1,2,3,4,的值依次为 1,2,
3,4,由主函数输入。求 x在 1附近的一个实根。求出根后由主函数输出。
8.12 输入 10个学生 5门课的成绩,分别用函数求,① 每个学生平均分 ;② 每门课的平均分 ;③ 找出最高的分数所对应的学生和课程 ;④ 求平均分方差,σ=1n∑x2i-
(∑xi/n)2,xi为某一学生的平均分。
8.13 写几个函数,① 输入 10个职工的姓名和职工号,②
按职工号由小到大顺序排序,姓名顺序也随之调整 ;③ 要求输入一个职工号,用折半查找法找出该职工的姓名,从主函数输入要查找的职工号,输出该职工姓名。
8.14 写一函数,输入一个十六进制数,输出相应的十进制数。
8.15 给出年、月、日,计算该日是该年的第 n天。