1
第 8章 函数
2
8.1 概述到目前为止,我们涉及到的只是C编译系统提供的标准库函数,例如标准 I/O函数 getchar、
scanf ….,数学函数 sin,cos,sqrt 字符与字符串处理函数 strcmp,strcpy … 等等。
这一章中我们将讨论如何自己去定义、编写、使用各种不同的新函数。
3
什么是函数?
函数是完成特定功能的代码段。
某些函数执行指定的动作,如 printf( ) 将给出的数据按指定的格式显示在屏幕上 ;
一些函数返回一个值供调用者使用,如 strlen( )
将指定的字符串长度传递给调用程序;
一般而言,一个函数可能同时具备以上两种功能。
4
为什么要使用函数?
函数的使用可省去重复代码的编写。如果程序中需要多次使用某种特定功能,那么只需编写一个合适的函数即可。如要计算:
xxx
fff
s
xxx
eee
f
xxx
321
321
s i n3s i n21s i n
321
9.6
424
22
22




5
double y ( double x )
{
double value ;
value = x*x+sqrt ( 1.0+2.0*x+3.0*x*x ) ;
return value ;
}
可以先编写计算 x 2 xx 321 2 的函数,
然后再编写如下的主函数,调用函数 y,
6
#include <stdio.h>
#include <math.h>
main( )
{
double y ( double x ) ;
double f,s,x ;
scanf (,%f,,&x ) ;
f = 6.9/y ( exp (x) ) + y ( sin (x) ) ;
s=y ( f*f ) / y ( sqrt ( x ) ) ;
printf (,f=%f\ns=%f\n”,f,s ) ;
}
7
如何正确定义函数;
如何正确调用函数;
函数执行完如何返回;
如何在函数间传递数据 (函数间数据通信 )。
对函数需要了解什么?
8
1、函数分类
1) 根据 定义 分类系统预定义库函数用户自定义函数
2) 根据 参数 分类无参数函数有参数函数
3) 根据 返回值 分类有返回值函数无返回值函数
9
4) 根据函数的 文件作用域 分类
static函数
extern函数
5) 根据函数的 实现方法 分类递归函数非递归函数
6) 根据函数的 应用及功能 分类数学函数字符串处理函数字符处理函数
……
10
2、函数定义、调用与返回一个 C源程序可由若干函数组成 (多个相互独立的函数定义 ),但必须有一个也只能有一个 main函数定义 (函数名是规定的,函数体是自定义的 ),函数不允许嵌套定义 (函数中不能再定义另一个函数 )。
C程序中所有函数间的关系是平等的。 每个函数都可以调用其他函数,或被其他函数调用,或自己调用自己 (递归调用 )。 main函数一般只由操作系统调用它,引起程序的执行。
函数被调用执行中当 遇到 return语句或函数体最后的,}” 时,将返回到主调函数处或调用点的下一条语句继续执行。返回时可有返回值或没有返回值。
11
8.2 函数定义的一般形式
1、函数定义格式函数有两种定义格式,原型声明格式 和 传统声明格式 (建议按原型声明格式定义函数 )
a) 原型声明格式
[存储类型 ] [函数类型 ] 函数名 (形式参数声明表 )
{
局部变量声明序列语句序列
}
12
void show_n_char ( char ch,int num )
{
int count ;
for ( count = 0 ; count <= num ; count++ )
putchar ( ch ) ;
}
例 1:
13
imin ( int n,int m )
{
if ( n < m )
return n ;
else
return m ;
}
例 2:
/* 不能写成 imin ( int n,m ) */
14
[存储类型 ][函数类型 ] 函数名 (形式参数表 )
形式参数声明
{
局部变量声明序列语句序列
}
例,imin ( n,m )
int n,m ;
{
if ( n < m )
return n ;
else
return m ;
}
b) 传统声明格式
15
int samename ( int n )
{
int samename = 2 ;
return n%samename ;
}
main ( )
{
int a = 17,b ;
b = samename ( a ) ;
printf (,b=%d\n”,b) ;
}
函数名 按标识符的规则确定。 函数名是一种外部全局性对象,所以不能与该程序中的其他函数名及外部对象同名。 但可以与任何函数 (包括该函数 )中的局部变量同名,不会产生冲突。
例如:
16
指的是函数 返回值的数据类型 。允许基本类型、指针类型、结构类型、联合类型和 void 。
若函数无返回值 (无值型 函数 ),类型处放置 void
关键字 。 这种函数只能作为表达式语句调用,函数体中不能使用带返回值 return语句,若函数的执行必须在某个位置返回,此时仅 可以使用不带返回值的
return语句返回。
若函数执行后 要返回一个值,则定义函数时 必须 在该 位置处 指定函数返回值的数据类型;
若定义函数时没有指定函数的类型,则函数类型为 int。 (注,C99不再支持函数的 int默认类型 )。
函数类型
17
例 1:
void show_n_char ( char ch,int num )
{
int count ;
for ( count = i ; count <= num ; count++ )
putchar ( ch ) ;
}
18
例 2:
imin ( int n,int m )
{
if ( n < m )
return n ;
else
return m ;
}
19
若 在,存储类型,位置处指定 static 关键字,
那么便定义了一个,静态函数,。
所谓,静态函数,即它的作用域仅限于定义它的源文件。 这样的函数只能由定义它的源文件中的函数调用,对于同一个程序的其他源文件中的函数来讲它是不可见的,当然是不可调用的。
static 函数的这种局部化特性隐含着在同一个源程序的其他源文件中定义的函数的名字可以与它同名,不会产生冲突。
存储类型
20
形式参数指出调用该函数时必须给出的参数,一个函数的形式参数的多少、顺序没有限制,形式参数之间用逗号分隔,也可以为空 (没有形式参数),此时 在形式参数表中只放一个 void,或者为空 (括号不能不写 )。
形式参数 只能是 变量名、数组名,且都必须声明,声明的 方法与变量相同。
形式参数的数据类型可以是 基本类型、指针类型、结构类型和联合类型。
21
形式参数的,存储类型,仅能是 auto 和 register。
形式参数的作用域是定义它们的函数,其特性与局部数据对象的特性相同,当函数每次被调用时为其分配存储空间而存在,并在退出函数时随着存储空间的回收而消亡,不能与该函数中其他局部变量同名 。
定义函数时 不能给形式参数指定初始化值,且形式参数不进行初始化便可使用,它们的初始化值是调用该函数时由对应的实在参数传递过来的值 。
22
atoi ( char s[ ] )
{
int i,n=0 ;
for ( i=0 ; s[i]>=?0? && s[i]<=?9? ; ++ i )
n=10*n+s[i] -?0? ;
return ( n ) ;
}
2、函数定义例例 1 把数字字符串转换成整数的函数 (原型格式)
该函数采用原型方式定义。没有指定函数类型,则函数的返回值被转换成 int 型。
/* 形式参数是数组时,
可不给出数组大小 */
23
binary ( x,v,n )
int x,v[ ],n ;
{
int low=0,high=n-1,mid ;
while ( low <= high ) {
mid = ( low+high ) / 2 ;
if ( x < v[mid] )
high = mid-1 ;
else if ( x > v[mid] )
low = mid+1 ;
else
return ( mid ) ;
}
return ( -1 ) ;
}
一个 二分搜索函数的定义 (传统定义格式)
其中:
形式参数 x 为要在数组 v[ ] 中搜索的对象形式参数 数组 v [ ] 的元素必须已经排序过数组作形式参数不必给出数组大小,如该函数中形式参数 v[ ],其大小可通过另一参数给出,如该函数中形式参数 n 。
例 2:
24
3、函数定义注意点
C 语言允许定义参数个数不定的函数 (如 printf的参数个数就是不定的 )。如果函数要接收不定量的参数,可把省略号( … )作为函数参数表中的最后一个参数。例如,
int printf ( char *format,...)
推荐使用函数原型声明方式,因为它允许编译程序在实际调用函数时检查参数的数目和类型,也允许编译程序在可能时对参数进行数据类型转换。
25
虽然函数可以用,传统,和,原型,两种方式声明,但是使用 函数的说明方式最好与函数的定义方式一致,否则结果不正确,这对浮点型的参数特别明显。
也可以给 main 函数指定返值类型。
以后会知道,也可以给 main 函数指定形式参数 。
26
int main ( void )
{
int imax ( int n,int m ) ;
printf (,The maximum of %d and \
%d is %d\n”,3,5,imax ( 3,5 ) ) ;
return 0 ;
}
例如:
/*引用函数说明 */
27
int imax ( int n,int m)
{
int max ;
if ( n > m )
max = n ;
else
max = m ;
return max ;
}
28
int f ( int w )
{
int i,j ;
int g ( int x )
{
int a = 0 ;
x = x*a++ ;
}
……
}
4、函数定义常见错误
a) 嵌套定义函数嵌套定义的函数
29
b) 形式参数表错误例,int f ( int x,y ) /*函数头部错误 (语法错 )*/
{ …… }
c) 例,int f ( int x,int y ) ; /*函数说明而非函数定义 */
{ …… }
d) 函数体中出现与形式参数同名的变量声明例,int f ( int x )
{ float x ; …… } /* 语法错 */
e) 试图用一个 return e ; 返回一个以上的值例,return x,y ; /* 仅返回 y,语法对,但语义错 */
return x; return y; /* return y;不可能执行 */
30
8.3 函数的值与及 return 语句当被调用函数执行到最后的,}” 处或遇到一条
return语句时,便返回到调用者。
若函数作为单一的表达式语句被调用,则返回到该表达式语句的下一条语句处执行;
若作为表达式中的运算分量调用,则返回到该函数调用的表式所在的语句,用返值代替调用形式。
非 void型函数总要返回给调用者一个值,返回值的类型由定义时给出的 函数类型 决定。
31
那么在函数中如何给出指定的返回值呢? 这一般是通过 return语句 来实现的。
return 语句的一般形式:
return (表达式 ) ;
或 return ;
其中的,表达式,就是指定的返回值。表达式及其两边的小括号可有可无。
return 语句有三个用途:
其一,用于结束函数的执行并返回到调用者其二,用来向调用者传递回一个函数类型的返值。
其三,用来归还函数的局部变量的存储空间。
32
return 语句中的,表达式,的数据类型应该与该函数的类型一致。 如果不一致则,表达式,求值后被 自动转换 成函数的数据类型,再传递给调用者;
如果一个函数在定义时没有指定其数据类型,则返回的,表达式,被自动转换成 int 型后再传递给调用者 ;
如果定义函数时没有指定其返值类型,在函数中也没有使用 return 语句,或仅仅给出了一条不带表达式的,return”语句,那么对于 int 型函数将返回给调用者一个 0值 ;对于其他类型的函数,返回值不确定,大多数情况导致程序运行出错。
33
函数中可以根据需要在多个不同位置安排具有不同返值的 return 语句。 反之,函数中并非必须要
return 语句,没有任何 return 语句的函数仅当函数执行到最后的大括号时才返回到调用者。
main 函数中也可以 return 语句给操作系统返回一个值。通常使用 return 0 或 return 1 给操作系统返回一个整型值。
34
8.4 函数的调用在程序中,一个函数调用另一个函数意味着将程序转移到被调用函数执行。
要正确实现函数间的相互调用必须满足一定的条件,
第一 被调用函数必须存在且允许调用;
第二 必须给出满足函数运行时要求的参数;
第三 在调用一个函数前必须对被调用函数进行说明 ;
第四 必须按指定的格式调用。
35
函数说明形式对应于函数的声明形式也有两种:
传统函数说明方式,
存储类型 数据类型 函数名 ( );
在这个说明语句中,,存储类型,,,数据类型,必须与该函数定义时指定的存储类型和数据类型一致。
,函数名,即要被调用的函数名,其后的小括号不可丢掉,且一般为空。
因为函数的传统说明形式只给出了函数的返值数据类型,并未给出任何参数信息,所以不利于函数调用时参数一致性的检查。
例如:
36
int imax ( n,m)
int n,m ;
{
int max ;
if ( n > m )
max = n ;
else
max = m ;
return max ;
}
这是传统式函数定义形式
37
#include <stdio.h>
int imax ( ) ; /* 调用函数 imax 的 传统式说明 */
int main (void )
{
printf (,The maximum of %d and \
%d is %d\n”,3,5,imax (3) ) ;
printf (,The maximum of %d and \
%d is %d.\n”,3,5,imax ( 3.0,5.0 ) ) ;
return 0 ;
}
38
main函数中的第一个 printf 中调用 imax 时漏掉了一个参数,而第二次调用 imax 时使用的是浮点参数而不是整型参数。尽管存在这些错误,该程序仍可正常执行完成。 但程序的输出是:
The maximum of 3 and 5 is 3
The maximum of 3 and 5 is 0
出现这种结果的原因是:
函数调用时先把给出的 实参 放到一个称为“堆栈”的临时存储区域中,然后被调函数按函数定义时指定的 参数类型 从“堆栈”中读取这些参数。
39
main 先把 3 放到
“堆栈”中,当
imax
开始执行时,按函数形式参数定义从
“堆栈” 中读取两个
int整数。 但此时
“堆栈” 中实际上只有一个 int 参数
3,所以就把 3 后面的存储区域中的当前值作为第二个参数读出,读出数恰好是 0,所以输
40
同理,第二次 imax调用,先把 3.0和 5.0先转换成
double型数存储到“堆栈”中,imax则从“堆栈”
中读出两个 16位 int 数,第一个 16位为 0,拿到的第二个 16位也是全 0。所以输出为 0。
double 型数在微机中的用 64 位表示。表示方法是:
41
浮点数 3.0和 5.0 分别表示为:
3D = 11B = (1.)1× 21
f = 100000000000000000000000000000000000000000000000000
e =1023+1 = 1024D = 10000000000B
s = 0
则 3.0机内的 double表示是:
010000000000 100000000000000000000000000000000000000000000000000
同理:
5D = 101B=(1.)01× 22
f=0100000000000000000000000000000000000000000000000000
e = 1023+2=1025D=10000000001B
s = 0
则 5.0 机内的 double表示是:
010000000001 010000000000000000000000000000000000000000000000000
42
43
以上的参数类型错误匹配问题,解决的办法就是使用 函数原型说明 方式来声明调用的函数。这种说明加上了函数参数类型、函数返值类型等信息。
编译程序就可据此检查函数调用语句是否和原型声明一致,若类型不 匹配则会自动转换成形式参数的类型。
对调用函数的原型说明方式:
数据类型 函数名 (参数 1,参数 2,..,参数 n );
其中,参数 i”可以是如下三种形式之一:
数据类型关键字数据类型 参数名

44
char fun1 ( int,long ) ;
接收两个参数,它们分别为 int,long型数。
int fun2 ( int k ) ;
接收一个 int型参数。说明中指定了一个变量名 k,
其实 k 只是一个 虚设 的参数名,它 不必 与函数定义中给定的形式参数名 相同 。
void fun3 ( char *p1,int p2 ) ;
接收两个参数,分别为字符指针,int 型数。
例如:
45
double fun4 ( float a[20],char s[ ] );
接收两个参数,参数都为数组。不必指出数组大小。
float fun5 ( void ) ; /*无参数 */
由于C语言函数允许带有可变个数的参数 (如
printf 的参数个数就是不定的),所以函数原型说名也应能够说明参数不定的函数。如果函数要接收不定量的参数,可把省略号 (… )作为函数原型参数表中的最后一个参数。如,
int printf ( char *format,..,) ;
46
关于函数说明的注意点推荐使用函数原型说明格式,因它允许编译程序在实际调用函数时检查参数的数目、类型及函数的返值类型,也允许编译程序在可能时对实际参数及函数的返值类型进行数据类型转换。
看上去函数原型说明与函数定义的第一行形式似乎相同,其实它们有重要区别:,函数原型说明,
是说明语句,必须以分号结束,对它而言不存在函数体,也不对它说明的对象分配存储空间;而函数定义是一个整体,是一个外部对象定义。
函数仅能定义一次,但函数可以说明多次。
47
对 int 型函数不必进行调用说明。但对 void型函数则必须进行调用说明。
函数调用说明可以在调用函数的内部进行,也可以在调用函数的外部进行。
若在调用函数的内部说明,则这种说明的作用域仅仅为该函数;
若在调用函数的外部说明,那么说明的作用域与该说明语句在源文件中的位置有关:如果在源文件的开始处说明,则其作用域为整个源文件;如果在一个源文件中的两个函数之间说明,则该说明的作用域仅为该说明语句位置后的那些函数,而对该位置前的函数无效 。
48
由于标准库函数的原型说明都包含在相应的头文件中,所以要调用标准库函数必须在程序源文件的开头部分包含一条 #include预处理命令,将相关函数的头文件包含到源文件中来。如要调用 I/O函数库必须包含 #include <stdio.h> 预处理命令行;而如果要调用字符串处理函数,则必须在源文件中包含
#include <string.h> 等。
必须注意,虽然函数可以用,传统,和,原型,
两种方式说明,但是函数的说明方式最好与函数的定义方式一致,否则结果不正确,这对浮点型的参数特别明显。
49
有一种方法可以不使用函数原型声明却可达到函数原型声明的目的。这就是在首次调用函数之前对该函数进行完整的定义。这样函数定义部分就起到与函数原型声明相同的作用。这也就是为什么程序设计中通常把函数定义放在 main ( ) 之前的原因。
int imax ( int a,int b )
{
return a > b? a,b ;
}
int main ( )
{
……
z = imax ( x,50 ) ;
……
}
既是函数定义,也是它的原型说明例如:
50
float aver ( int x,int y,int z) ;
main ( )
{
int x=1 ;
printf (,%f,,aver ( x+1,x,x++) ) ;
}
float aver ( int x,int y,int z )
{
return ( x+y+z )/3 ;
}
当函数定义在后,函数调用在前时需要在调用之前说明该函数;反之则不需说明。
例如,/* 调用 aver
函数说明 */
/* 函数定义在后 */
该程序的输出是,2.000000
51
缺少函数说明时的编译器处理方式首先遇到函数定义时,保存该函数的返回值类型和参数的个数及类型,用于后面调用该函数时检查实参的个数及类型。
首先遇到函数调用时,将函数的返回值类型设定为 int,当以后遇到该函数定义,若函数类型为非 int,则编译报错:
type mismatch in redefinition of …
52
考察下面两个程序,看上去是相同的,但执行结果是不同的。
#include <math.h>
main( )
{ int i ;
i=sizeof( sin(1) ) ;
printf (,%d\n”,i ) ;
}
在 <math.h>中有 sin(x)
的原型说明:
double sin (double x) ;
程序输出是,8
main( )
{ int i ;
i=sizeof( sin(1) ) ;
printf (,%d\n”,i ) ;
}
没有包含 <math.h>文件即没有对 sin(x) 进行说明,所以在程序中遇到
sin(1) 函数调用时,认为其返值类型为 int,故程序输出为,2
53
非 void 型函数总是要返回值的,因此非 void 型函数调用形式本身代表一个值,它可以作为一个运算分量参于表达式的运算,也可以在其后加上一个分号形成表达式语句。
应切记:因为 void型函数不返回值 (无值 ),所以不能将 void函数的调用形式作为表达式中的运算分量,它们仅能作为表达式语句来使用。
54
函数调用形式函数调用不同于简单的无条件程序控制转移,它采用如下规定的函数调用形式:
函数名 ( 实在参数表 )
被调用函数的名字调用函数给出的参数。对应于该函数定义时给出的,形式参数表,
指形参与实参在,
个数、顺序、类型上的一致性
55
可以是,
实在参数( 简称实参)
常量,变量,数组元素名,数组名
、表达式、带返回值的函数调用实参表中可以出现相同的实参,只要与形式参数对应。 例如:
因实参可以是表达式,且可相同,所以对诸实参存在求值的顺序问题。求值顺序不同会导致实参求值结果的不同,对此应引起注意。
不同的 C 编译程序中对实参的求值顺序可能不同,
有些从左往右求值,而另一些则可能从右往左顺序求值 (TC自右向左 )。 例如:
58
8.5 函数的嵌套调用在C语言中函数之间都可以相互调用。如果在函数 a中调用了函数 b,而在函数 b中又调用了函数 c,这样就形成了函数的嵌套调用。 例如:
main ( )
{
……
fun1(…);
……
}
fun1 ( )
{
……
fun2(…);
……
}
fun2 ( )
{
……
fun3(…);
……
}
59
例,计算,M! ÷ N! × ( M-N )!
该例中有 3个阶乘运算,为简化程序设计,应先编写一个计算阶乘的函数,
long fact ( long n )
{
long f=1 ; int k ;
for ( k=1 ; k<=n ; k++ )
f *= k ;
return f ;
}
60
long compose ( int x,int y )
{
return fact (x)/( fact (y)*fact (x-y) ) ;
}
为说明函数的嵌套调用,不妨把例中的计算公式也编写成一个函数(其实不需要这样做,除非复杂的计算中有一些相同的类似运算)。
compose函数中调用了计算阶乘的函数 fact
61
#include <stdio.h>
long compose ( int x,int y ) ;
long fact ( int n ) ;
main ( )
{
int m,n ;
scanf (,%d%d”,&m,&n) ;
printf (,c=%ld\n”,compose ( m,n ) ) ;
}
main 函数中再调用 compose 函数,获得计算结果。
/*调用
compose与
fact函数的引用说明 */
62
8.6 函数的递归调用
1) 递归概念在函数的嵌套调用中,如果在 A函数中调用了
B 函数,而在 B函数中又调用了 A函数,这样 A函数通过 B函数间接地调用了自己,这种调用称之为函数的 间接递归调用 。
另一种更为明显的情况是在一个函数中又出现了调用自己的函数调用。这种调用称之为 直接递归调用 。
函数的间接递归调用和直接递归调用统称为函数的递归调用。
65
long f ( long n )
{
if ( n==1 )
return 1 ;
else
return n * f ( n-1 ) ;
}
2) 递归函数例
n! 的递归定义,
计算 4!
n ! = 1n*(n-1) ! ( n = 1 )( n > 1 )
例 1:
递归函数定义 递归函数调用
66
f (long n ) 4? n
{ if (n==1)
return 1;
else
return 4*f (4 -1 );
}
f (long n) 3? n
{ if (n==1)
return 1;
else
return 3*f (3 -1 );
}
f (long n ) 2? n
{ if (n==1)
return 1;
else
return 2*f (2 -1 );
}
f (long n ) 1? n
{ if (n==1)
return 1;
else
return 1*f(1-1);
}
返回值,1
返回值,2
返回值,6返回值,24
printf (“%ld\n”,y ) ;
main ( )
{
y = f(4) ;
long y ;
}
主函数,
运行结果,
24
67
void printd ( int n )
{
int i ;
if ( n<0 ) {
putchar ( '-' ) ;
n = - n ;
}
if ( ( i=n/10 ) != 0 )
printd ( i ) ;
putchar ( n%10+'0' ) ;
}
例 2,利用递归函数,把任一 int型数转换成对应的字符串形式,并打印出来。
68
#include <stdio.h>
main( )
{
void print ( int n) ;
int n=123 ;
print (n) ;
}
print ( int n )
{
int i ;
if (n<0)
{
putchar (?-? ) ;
n=-n;
}
if ( (i=n/10)!=0 )
print ( i ) ;
putchar ( n%10+?0? ) ;
}
123
输出 3
69
print ( int n )
{
int i ;
if (n<0)
{
putchar (?-? ) ;
n=-n;
}
if ( (i=n/10)!=0 )
print ( i ) ;
putchar ( n%10+?0? ) ;
}
1
print ( int n )
{
int i ;
if (n<0)
{
putchar (?-? ) ;
n=-n;
}
if ( (i=n/10)!=0 )
print ( i ) ;
putchar ( n%10+?0? ) ;
}
12
输出 1输出 2
70
用二分法求方程 x2-x-2=0 在区间 [1,4]中的一个根 。
例 3,
所谓二分法指每次将区间折半,计算折半后的两个小区间的端点函数值,舍去两端函数值同号者 (在 x轴的上方或下方 ) 。如:
0 a bC=(a+b)/2
f(a)
f(b)f(c)
图中区间 [c,b]的 f(c)与 f(b)同号舍去,对 [a,c]区间再作二分,依此类推 ……
71
double f ( double x )
{
}
……
编写计算函数值的函数
return x*x-x-2 ;
double root ( double a,double b )
{
}
编写求方程根的函数形式参数 a,b
为给定的区间
double m = (a+b)/2
定义变量 m,
初值为 给定区间的中点
,f0 = f(a)
定义变量 f0,
初值为 给定区间的左端点函数值
,y=0 ;
定义变量 y,作为所求的根,
初值为 0
if ( fabs (a-m) >= 1e-6 )
如果区间还比较大,继续二分当前区间 ;如果区间已非常小,
则 m即所求的根
if ( f0*f(m) > 0 )
如果左区间两端点的函数值同号,则根在右区间,继续二分右区间 ;否则根在左区间,继续二分左区间 ;
y = root ( m,b ) ;
调用 root函数,
继续 在 右区间求根
else
y = root ( a,m ) ;
调用 root函数,
继续 在左 区间求根
else y = m ;
区间已非常小,则 m即所求的根
return y ;
返回求得的根
72
#include <math.h>
main ( )
{
double f (double x),root (double,double),a,b ;
scanf ( "%lf,%lf ",&a,&b ) ;
printf ( "\n The root is %lf\n",root ( a,b ) ) ;
}
程序运行时的输入:
1,4
程序运行后的输出:
The root is 2.000000
76
int gcd ( int a,int b )
{
int r ;
if ( ( r=a%b)==0 )
return b ;
else
return gcd ( b,r ) ;
}
例 5:求两个整数的最大公约数把上次的的除数作为被除数,把上次两个数相除的余数作为除数给出的两个整数 a,b
a作为被除数,
b作为除数结束递归的条件判断
77
3) 递归函数的工作原理与结构用递归函数解决复杂的问题时,每次递归调用时要解决的问题都要比上次调用简单,数据规模越来越小,最终归结到基本问题(已知问题)。
解决完基本问题后,函数沿着调用顺序逐级返回上次调用一个结果,直到函数的原始调用处为止。
递归函数一般由一个选择结构组成,
条件为真的部分计算基本问题,终止递归部分 ;
条件为假的部分 简化问题继续调用部分 。
78
4) 递归函数的性能使用递归函数能够把程序写的十分简洁、紧凑。
但是,具有递归性质的函数也有其不足之处:
第一,尽管程序简洁,但不便于理解;
第二,采用递归方法编制的函数没有提高程序的质量,相反它的开销会更大。
指处理递归所需的时间和空间第三,程序的运行速度要比用非递归程序慢。
编写递归函数时需要注意,
要防止递归函数 无休止地 反复调用自己,在函数内必须设立某个条件,对这种反复调用进行控制,
保证能终止这个反复调用过程,使函数能逐层返回。
79
double f1 ( double x,int n )
{
double s=0,t=1 ;
int k ;
for ( k=1 ; k<=n ; k++ ) {
t *= x/k ;
s += t ;
}
return s ;
}
5)递归与递推比较例:函数 f1 计算以下公式的值:
s = x + x2/2! + x3/3! + …+ x n/n!
[ 递推法]
K t
1 1*x/1=x
2 x*x/2=x2/2= x2/2!
3 x2/2!*x/3=x3/6= x3/3!
4 x3/3!*x/4=x4/4!
5 ………
80
long f ( int n )
{ int i,s=1;
for ( i=1; i<=n ; i++ ) s*=i ;
return s ;
}
double f1 ( double x,int n )
{
if ( n==1 )
return x ;
else
return f1 ( x,n-1 )+pow ( x,n )/ f(n) ;
}
[递归法 ]
81
#include <stdio.h>
#include <math.h>
main( )
{
long f( int n ),x=2,n=4 ;
double f1( double x,int n ) ;
printf (,%lf\n”,f1( x,n ) ) ;
} 2+ 22 /2!+23/3!+24/4!
f1( 2,4 )
86
两种方法的比较:
a) 一般用递归能够解决的问题,用都能用递推解决;
反之也成立。但建议不要用递归解决。
b) 递推 使用循环结构,递归使用选择结构。
c) 递推 和递归都实现了循环。
递推,循环执行函数中的一段程序代码 (循环结构 )
递归,循环执行整个函数体的程序代码 (循环调用 )
d) 递推 和递归都有可能出现无穷情况( 死循环 )。
87
8.7 数组作为函数参数
C 语言函数间的数据传递可以采用如下手段,
实参传递给形参函数的返值全局变量
(基本手段)
88
1,实参传递给形参
C语言中函数调用时将实参传递给形参都是
“赋 值调用,方式。
在函数调用时,将给出的实参传递给对应的形参,使函数获得要求的参数,是函数间传递数据的基本手段。
即调用函数时,
把实参的值赋给被调用函数对应的形参变量。
这种参数传递方式是 复制型 的函数之间进行,单向,数据传递函数执行中对 形参变量本身 的任何修改都 不会影响或改变 给出的实参的值。
89
void swap ( int x,int y )
{ int z ;
z = x ;
x = y ;
y = z ;
}
main ( )
{ int a=1,b=2 ;
swap ( a,b ) ;
printf (,a=%d,b=%d”,a,b ) ;
}
程序输出,a=1,b=2 (并没有交换 !)
(1) 函数间变量的传递函数间传递变量是一种最简单的情况,仅举一个例子说明之 。
90
main 函数调用 swap 函数时,将实参 a,b
的值复制到对应形参变量 x,y 中,形参变量 x、
y 中的值发生变化时,对应实参 a,b中的值并不改变 。
main函数 swap函数局部变量 a
局部变量 b
形参变量 x
形参变量 y
1
2
1
2
1
2
堆栈实参
91
(2) 函数间数组的传递
a) 数组名作为函数参数实参是数组名时,形参也必须是相同类型的数组形式,或 在第 10章中要讲的相同类型 指针变量 名
void fun ( char str[ ] )
{
int i ;
for(i=0;str[i]!=?\0?;i++)
putchar(str[i]);
}
#include <stdio.h>
void fun ( char str[ ] );
main( )
{
char tring[]=“Thanks”;
fun( tring );
……
}
形参是 char
型的数组形式实参也是
char型的数组
92
形式参数是数组形式时,数组名不是地址常量,而是一种变量名(即在第 10章中要讲的 指针变量)。而且不论给其指定数组大小与否,都 不分配相应的数组存储空间,只给该数组名分配存储空间 。
上例中,实参 string 数组名传递给形参组名 str时,
必定执行 string str 的赋值操作,这显然与数组名是一个地址常量不能向它赋值相矛盾。因此这里要特别说明,
93
形式参数是数组形式时,传递的仅是数组的起始存储地址而不是数组的全部元素 。 形参数组与实参数组共享同一存储空间。
FFD0
main 函数中 fun 函数中的形分配给局部数组 string的存储空间起址为,
string
T h a n k s 0
FFD0
参数组名 str并非数组,而是变量,
分配存储单元为,
FFD2 FFD3FFD0 FFD1 FFD4 FFD5 FFD6
堆栈
str使用 string的存储空间
str
94
形参数组的大小是由实参数组的大小决定的。为了让被调用函数知道要处理实参数组的多少个元素,
应该另设一个形式参数指出实参数组的大小。
void display ( int num[10],int n )
{
int i ;
for ( i=0 ; i<n ; i++ )
printf ( "%d\n",num[i] ) ;
}
例如:
形参数组的大小由调用该函数时给出的实参数组的大小决定。
这里给出的 形参数组的大小是 无意义的!
通常都不给出,
即写成,int
num[ ]
int n指出实参数组的大小。或者说用于指出可以使用实参数组中多少个元素。
95
#include <stdio.h>
main ( )
{
viod display ( int num[ ],int n ) ;
int t[10],n=10,i ;
for ( i=0 ; i<10 ; ++i )
t[i]=i ;
display ( t,n ) ;
}
96
因实参数组与形参形式的数组都使用同一个地址空间,所以函数中对形参数组元素的修改也必定改变相应实参数组的元素的值。程序设计中可以利用这一特性改变实参数组元素的值。
例如,
void atob ( int a[ ],int n,int b[ ] )
{
int i ;
for ( i=0 ; i<n ; i++ )
b[i]=a[i]+10 ;
}
函数中改变了形参数组 b 中 元素的值,
实际上就是改变了实参数组 w中对应元素的值。
97
main ( )
{
void atob ( int a[ ],int n,int b[ ] ) ;
int i,k[8]={ 1,2,3,4,5,6,7,8 },w[8] ;
atob ( k,8,w ) ;
for ( i=0 ; i<8 ; i++ )
printf ( "%2d",w[i] ) ;
printf ( "\n" ) ;
}
程序输出是,11 12 13 14 15 16 17 18
虽然数组 w 中的元素开始时没有初值,但调用了函数 atob
后,atob 函数中已改变了形参数组 b中 元素的值,实际上就是改变了实参数组 w中对应 元素的值。
98
函数中使用形参数组是按给出的数组大小从给出的实参数组的起始位置开始使用实参数组中的元素。
例如,
void atob ( int a[ ],int n )
{
int i ;
for ( i=0 ; i<n ; i++ )
a[i]+= 10 ;
}
99
main ( )
{
void atob ( int a[ ],int n ) ;
int i,k[8]={ 1,2,3,4,5,6,7,8 } ;
atob ( k,5 ) ;
for ( i=0 ; i<8 ; i++ )
printf ( "%2d",k[i] ) ;
printf ( "\n" ) ;
}
程序输出是,11 12 13 14 15 6 7 8
虽然 k数组有 8个元素,但只允许
atob 中只使用前 5个元素。
100
形参数组并不必要从实参数组的开始地址使用实参数组的元素,而可以从实参数组的某个指定的元素位置开始使用指定的个数。
void atob ( int b[ ],int n )
{
int i ;
for ( i=0 ; i<n ; i++ )
b[i]+=10 ;
}
例如,
101
main ( )
{
void atob ( int b[ ],int n ) ;
int i,a[8] = { 1,2,3,4,5,6,7,8 } ;
atob ( &a[2],3 ) ;
for ( i=0 ; i<8 ; i++ )
printf ( "%3d",a[i] ) ;
printf ("\n") ;
}
让 atob函数只从 a数组的 &a[2] (注意,一定要是某个元素的地址) 位置起使用 3个元素。
程序输出是,1 2 13 14 15 6 7 8
102
形式参数是数组形式时,它所需要的实参只要是同类型对象的 存储区域的地址,都可以接收 。
特例,
void k(int b[ ],int n)
{
int i ;
for(i=0 ; i<n ; i++)
b[i]+= 15 ;
}
main ( )
{
void k(int b[ ],int n) ;
int a= 100 ;
k ( &a,1 ) ;
printf ( "%d\n",a ) ;
}
程序输出是,115
让 k函数只从 a变量的存储地址开始使用 1个元素。
103
b) 数组元素名作为实在参数数组元素作函数实参,其用法与变量完全相同,
实参传递给形参也是,单向赋值传递,,传递的是数组元素的值。函数中改变相应形参的值,也不会改变对应的数组元素的值。
假定有数组 a和 b,现要分别比较它们的对应元素,若 a[k] > b[k] 的情况多于 b[k] > a[k] 的情况,
就说数组 a 大于数组 b,否则就说数组 b大于数组 a。
例如:
程序如下:
104
main ( )
{ int a[10],b[10],ki,gt=0,eq=0,lt=0 ;
printf ( "Enter array a:\n,) ;
for ( ki=0 ; ki<10 ; ki++ )
scanf ( "%d",&a[ki] ) ;
printf ( "\nEnter array b:\n,) ;
for ( ki=0 ; ki<10 ; ki++ )
scanf ( "%d",&b[ki] ) ;
for ( ki=0 ; ki<10 ; ki++ )
if ( large(a[ki],b[ki] ) == 1 ) gt++ ;
else if ( large(a[ki],b[ki])== 0) eq++ ;
else
lt++ ;
数组元素名作为实在参数
105
printf (,a[ki]>b[ki] %d times\n”,gt ) ;
printf (,a[ki]=b[ki] %d times\n”,eq ) ;
printf (,a[ki]<b[ki] %d times\n”,lt) ;
if ( gt>lt )
printf (,Array a is larger than array b.\n” ) ;
else if ( gt==lt )
printf (,Array a is equal to array b.\n” ) ;
else
printf (,Array a is smaller than array b.\n” ) ;
}
106
程序运行,
a[ki]>b[ki] 4 times
a[ki]=b[ki] 1 times
a[ki]<b[ki] 5 times
Array a is smaller than array b.
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
107
int large ( int x,int y )
{
int flag = 0 ;
if ( x>y )
flag = 1 ;
else if ( x<y )
flag = -1 ;
return flag ;
}
a[ki] b[ki]
108
c) 多维数组名作为函数实参当多维数组名作为调用函数的实参时,被调用函数的形参也必须是相应维数的多维数组形式。
例如:
void fun ( int array[2][3],int rows )
{
……
}
main( )
{ int k[2][3]={ {1,2,3},{4,5,6} } ;
……
fun ( k,2 ) ;
……
}
二维数组形式的形式参数指出给出的实参二维数组有多少行
k和 2是调用 fun函数时给出的实参。
k为二维数组名,
2 是行数 。
109
在定义函数时,若函数的某个形式参数为多维数组形式时,除了第一维 (最左边的维 )的大小可以不给 ([ ]中为空 )外,其他都必须给出具体的数值。
例如:
void fun ( int array[3][10],int rows )
{
……
}
或 void fun ( int array[ ][10],int rows )
{
……
}
上面二者都正确,且等价。
这里给出的大小是无意义的!
其大小由另一个形式参数指定,该例中由第二个形式参数 rows指出。
110
下面的 二维数组形式的 形式参数声明是错误的:
void fun ( int array[ ][ ],int rows )
{
……
}这里不能为空!必须指出一行中有多少个元素。这样编译系统可根据从实参传送来的 数组起始地址,
行数 及 此值 决定该数组有多少行,多少列。
111
调用一个具有多维数组形式的形参函数时,调用说明中 的多维数组形式参数的说明可以有如下几种形式:
void fun ( int array[3][10],int rows ) ;
/* 可以忽略其中的 3 */
void fun ( int array[ ][10],int rows ) ;
/* 标准形式 */
void fun ( int [ ][10],int ) ;
/* 可以省略数组名 */
void fun ( int (*array)[10],int ) ;
/* 指针的形式 */
112
无论上述哪种形式的多维数组说明,实际上都表示 array指向一个二维数组,该二维数组的每一行由
10个 int型数据构成 。
array 具有二维数组名的性质,即有,
array[0] 等价于 array 等价于 &array[0] [0]
array[1] 等价于 array+1 (即下一行的起址)
……
下面的几个例子说明了函数间如何传递二维数组。
113
例 1:
#define ROWS 3
#define COLS 4
void sum_rows ( int ar[ ][COLS ],int rows ) ;
void sum_cols ( int [ ][COLS],int ) ;
int sum1 ( int (*ar)[COLS],int rows ) ;
int sum2 ( int ar[ ],int cols ) ;
114
main ( )
{
int i,total=0,junk[ROWS][COLS]={ {2,4,6,8},
{3,5,7,9},
{12,10,8,6} } ;
sum_rows ( junk,ROWS ) ;
sum_cols ( junk,ROWS ) ;
printf("Sum of all elements=%d\n",
sum1 ( junk,ROWS ) ) ;
for ( i=0; i<ROWS; i++ )
total += sum2 ( junk[i],COLS ) ;
printf ( "Sum of all elements=%d\n",total ) ;
}
115
void sum_rows ( int ar [ ][COLS],int rows )
{
int r,c,tot=0 ;
for ( r=0 ; r<rows ; r++ )
for ( c=0 ; c<COLS ; c++ )
tot += ar[r][c] ;
printf ( "Sum of all elements=%d\n",tot ) ;
}
junk ROWS
116
void sum_cols ( int ar[ ][COLS],int rows )
{
int r,c,tot=0 ;
for ( r=0; r<COLS; r++ )
for ( c=0; c<rows; c++ )
tot +=ar[c][r] ;
printf ( "Sum of all elements=%d\n",tot ) ;
}
junk ROWS
117
int sum1 ( int ar[ ][COLS],int rows )
{
int r,c,tot=0 ;
for ( r=0 ; r<rows ; r++ )
for ( c=0 ; c<COLS ; c++ )
tot += ar[r][c] ;
return tot ;
}
junk ROWS
118
int sum2 ( int ar[ ],int cols ) /* ar[ ]是一维数组 */
{
int c,tot=0 ;
for ( c=0 ; c<cols ; c++ )
tot += ar[c] ;
return tot ;
}
junk COLS
119
程序运行输出:
Sum of all elements=80
Sum of all elements=80
Sum of all elements=80
Sum of all elements=80
120
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25
调用程序希望被调用的函数只处理给出的二维数组的 前几行与前几列 。
例 2:
例如,
121
int i,j,a[5][5] ={
{ 1,2,3,4,5 },
{ 6,7,8,9,10 },
{ 11,12,13,14,15 },
{ 16,17,18,19,20 },
{ 21,22,23,24,25 }} ;
main ( )
{ void agtb ( int b[ ][5],int row,int col ) ;
agtb ( a,4,4 ) ;
for ( i=0 ; i<5 ; i++ ) {
for ( j=0 ; j<5 ; j++ )
printf ( "%3d",a[i][j] ) ;
printf ("\n") ;}
}
122
void agtb ( int b[ ][5],int row,int col )
{
int i,j ;
for ( i=0 ; i<row ; i++ )
for ( j=0 ; j<col ; j++ )
b[i][j] /= b[i][j] ;
}
a 4 4
123
1 1 1 1 5
1 1 1 1 10
1 1 1 1 15
1 1 1 1 20
21 22 23 24 25
程序输出是:
124
例 2:
调用程序希望被调用的函数只处理给出的二维数组的 中间几行与中间几列。
例如,
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25
125
main ( )
{
for ( i=0; i<5; i++ )
for ( j=0; j<5; j++ )
scanf ( "%d",&a[i][j] ) ;
f ( a[1],3,1,3 ) ; /* endrow,startcol,endcol */
for ( i=0; i<5; i++ ) {
for ( j=0; j<5; j++ )
printf ( "%3d",a[i][j] ) ;
printf ("\n") ;
}
int i,j,a[5][5] ;
void f(int b[ ][5],int endrow,int startcol,int endcol ) ;
}
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25
a[1]开始行开始列 1
结束行 3
结束列 3
126
void f( int b[ ][5],int endrow,int startcol,int endcol )
{
int i,j ;
for ( i=0 ; i<endrow ; i++ )
for ( j=startcol ; j<=endcol ; j++ )
b[i][j] -= b[i][j] ;
}
开始行
a[1]
结束行
3
开始列
1
结束列
3
127
程序执行后的输出:
1 2 3 4 5
6 0 0 0 10
11 0 0 0 15
16 0 0 0 20
21 22 23 24 25
130
8.8 局部变量和全局变量局部变量所谓局部变量,是指变量的作用域仅是定义它的函数或复合语句。
例如:
三个不同位置
(3) 复合语句内定义的量仅在该复合语句内有效。
(2) 函数的形式参数也是局部量,它仅在定义它的函数中有效。
(1) 不同函数中以相同名字定义的量代表不同的对象,互相没有关系。 main函数也是一个函数,所以 main函数中定义的量也只在 main函数中有效。
131
main ( )
{
int a,b,c ;
…….
if ( a>b ) {
int a,b,c ;
…….
} ……,
}
float fun ( float a,float b,float c )
{……
}
char fun ( char x,char y,char z )
{
char a,b,c ;
}
132
main ( )
{ int a=3,b=2,c=1 ;
if ( a>b ){
int a=10,b=12,c=13 ;
printf ( "a=%d in compound statement.\n",a ) ;
}
printf ( "a=%d in main.\n",a ) ;
}
程序输出:
a = 10 in compound statement.
a = 3 in main.
133
全局变量也称外部变量,即在 函数之外声明 的量。它可以被在其作用域内的 其他函数所公用 。
利用形参与实参在函数间传递数据需要考虑参数类型的匹配、函数间数据实际传递所花的时间开销等问题。但可以利用全局对象对程序中的函数都存在、可访问的特性来方便地解决这些问题。
利用全局对象实现函数间的数据通讯是一种存储区域共享的办法,在函数间没有直接的数据传递动作。也不需要使用形式参数,但需事先,约定,。
下例说明这种方法的实际应用。
134
char b[4][3] ; /* 全局字符数组 */
main ( )
{
void atobmatrix ( char c[ ][4],int a ) ;
char a[3][4] = { { 'a','b','c','d' },{ 'e','f','g','h' },
{ 'i','j','k','l' } } ;
int i,j ;
atobmatrix ( a,3 ) ;
for ( i=0 ; i<4 ; i++ ) {
for ( j=0 ; j<3 ; j++ )
printf ( "%6c",b[i][j] ) ;
printf ( "\n,) ;
}
}
a b c d
e f g h
i j k l
程序输出:
135
void atobmatrix ( char f [ ][4],int a )
{
int i,j ;
for ( i=0 ; i<a ; i++ )
for ( j=0 ; j<4 ; j++ )
b[j][i] = f[i][j] ;
}
a 3
136
main函数中 a数组,
a b c d
e f g h
i j k l
程序输出,(全局数组 b)
a e i
b f j
c g k
d h l
该例按实参与形参传递的实现方式如下,
137
main ( )
{
void atobmatrix ( char c[ ][4],char b[ ][3],int a ) ;
char a[3][4] = { { 'a','b','c','d' },{ 'e','f','g','h' },
{?i?,?j?,?k?,?l? } },b[4][3] ;
int i,j ;
atobmatrix ( a,b,3 ) ;
for ( i=0 ; i<4 ; i++ ) {
for ( j=0 ; j<3 ; j++ )
printf ( "%6c",b[i][j] ) ;
printf ( "\n,) ;
}
}
把 b 定义成局部字符数组增加一个形式参数增加一个实参
138
void atobmatrix ( char f[ ][4],char b[ ][3],int a )
{
int i,j ;
for ( i=0 ; i<a ; i++ )
for ( j=0 ; j<4 ; j++ )
b[j][i] = f [i][j] ;
}
程序输出:
a e i
b f j
c g k
d h l
139
8.9 变量的存储类别
1,自定义标识符的作用域自定义标识符 指程序中由程序设计者自己定义的标识符。它们包括:
a) 用 #define预处理命令定义的各种符号常量名;
b) 各种变量名、数组名;
c) 函数名(包括库函数名)
d) 语句标号
e) 用户定义的数据类型名。
作用域 是指程序中自定义标识符的有效存取范围。
同一作用域内的所有自定义标识符都不能同名。
有四种作用域:
140
一个代码块是括在一对大括号 {……} 中的程序段。例如:整个函数体是一个代码块;一个函数体内包含的任一复合语句是一个代码块。
在代码块中定义的各种对象的作用域从定义该变量的地方到所在代码块的末尾。
尽管函数的形式参数是在函数体的开始大括号之前的位置定义的,但它们也具有代码块作用域。例如:
a) 代码块作用域 (block scope)
141
double block ( double c )
{
double d=0.0;
……
return d;
}
因此,其中的 c 和 d 作用域相同,因此它们不能同名形式参数C
也是代码块作用域
142
double block (double c)
{
double d=0.0 ;
int i ;
……
for ( i=0 ; i<10 ; i++ )
{
double q=c* i ;
……
d*=q ;
}
……
return d ;
}
q 的作用域
c,d,i 的作用域
143
double block (double c)
{
double d=0.0,q ;
int i ;
……
for ( i=0 ; i<10 ; i++ )
{
double d=c*i ;
……
q*=d ;
}
……
return q ;
}
这里的 d 和前面的 d 同名是允许的,它们的作用域不同!该域中使用的 d 是该复合语句中 d 的值。
这称之为变量的内层定义覆盖
144
int main ( )
{
int x=30 ;
printf (,x in outer block,%d\n”,x ) ;
{
int x=77 ; /* 新的 x,覆盖掉第一个 x */
printf (,x in inner block,%d\n”,x ) ;
}
printf (,x in outer block,%d\n”,x ) ;
while ( x++<33 ) {
int x=100 ; /* 新的 x,覆盖掉第一个 x */
x++ ;
printf (,x in while loop,%d\n”,x ) ;
}
}
每轮循环结束
(退出循环体的复合语句 ),其中定义的 x消失,
再次进入循环体后被重新定义成 100。
这里的 x是第一个 x定义。 但如果去掉循环体中的,int x=100 ;
定义,则使用的是每轮循环结束时 x的值
145
x in outer block,30
x in inner block,77
x in outer block,30
x in while loop,101
x in while loop,101
x in while loop,101
程序运行输出如下:
146
b) 函数原型作用域 ( function prototype scope )
这种作用域指的是函数原型 说明 中使用的变量名的作用域。例如:
int might ( int small,double large ) ;
small 和 large 的作用域仅到该行后的分号,编译器只关心该参数的类型,并不要求它们与定义函数时使用的相应形式参数变量名一样。
要 注意的是,函数原型说明 的作用域与说明它的位置有关:
若在函数内说明,其作用域是 代码块作用域 ;
若在函数外说明,其作用域是从说明位置起到源文件的末尾( 文件作用域 )
147
c)文件作用域 ( file scope )
文件作用域指源程序文件范围。 在函数外定义的 对象 具有 文件作用域,其作用域从它的定义点开始到文件的末尾。例如:
int units=0 ;
void critic ( void ) ;
main ( )
{
……
}
char ch=?a? ;
void critic ( void )
{
……
}
/*作用域是整个文件 */
/*作用域从该点到文件末尾 */
148
float PI = 3.14 ;
#define PI 3.1 /*PI的作用域从该点开始 */
main ( )
{
printf (,%f\n”,PI ) ;
}
符号常量的作用域是文件作用域。例如:
#define PI 3.1
float PI = 3.14 ;
main ( )
{
printf (,%d”,PI);
}
例 1:
例 2:
这儿有什么问题?
/*编译报错,左值问题 */
将被改成 3.10
程序输出,3.100000
149
d) 函数作用域 ( function scope )
这种作用域指的是 语句标号 的作用范围。
函数体内 非说明/声明语句 的前面都可以放置标号。语句标号一定在函数体内,它的作用域是整个函数。因此,函数体内不能有相同的语句标号。
对标号而言,不存在复合语句作用域问题。 这就意味着函数中可以用 goto语句无条件转到函数中的循环语句的循环体内,switch语句内,if语句内、
复合语句内,且按所转向的语句的控制功能执行。
150
2,C程序运行时使用的存储空间一个运行中的 C程序,除了程序代码占用的存储空间外,另外还使用到三类存放数据的存储空间:
静态数据区堆栈区寄存器存放 外部对象和 static 型的局部对象在编译阶段分配存储空间存放的 对象 相对程序的执行是永久的存放局部对象及函数的形式参数在函数或复合语句执行时分配空间存放的 对象 相对程序的执行是临时的存放 局部变量
151
3、存储类型关键字
auto 关键字只用于局部 量的声明(不能用于外部量的声明)
C编译程序总是将 auto 型变量存储到堆栈区这种变量具有局部性、动态性、临时性声明变量时若未给出存储类型关键字,对局部变量:
缺省值为 auto ; 而对外部变量缺省值为 extern 。
形参变量声明的存储类型只能是 auto和 register。
声明时若无初始化值则变量的初值不确定。初始化值可以是常量、在其前面已声明过的且具有初始化值的变量名、具有返值的函数调用、及由它们组成的表达式。
152
用来要求 C 编译程序把声明的变量存储器到计算机的寄存器中
register 关键字这种变量存取速度快只能用于局部变量及函数的形式参数变量的定义。
register 型变量与 auto型变量具有相同的性质若声明的这种变量的个数超过机器可供使用的寄存器数,C 编译系统会自动地将超过的部分作为
auto型变量处理,存放到堆栈区中因寄存器长度有限,不能将 long double 型的浮点数定义成 register型变量不允许对 register型变量进行取地址,&”操作
153
static 关键字
C 编译程序总是将这种变量、数组存储到到静态数据区 ; 这种变量、数组在整个程序的执行期间一直有效 ; C 语言允许定义静态局部变量和静态全局变量。
静态局部变量 局部特性与 auto 一样,与 auto
的区别在于,
(1) 存储的内存区域不同,auto 变量存储在堆栈区,而 static 型局部变量存储在静态数据区;
(2) static 型局部变量不随函数或复合语句的进入 /
退出而 建立 / 消亡。形象地说 static型局部变量有
“继承性”,即本次进入函数或复合语句使用的
static型局部变量的值是上一次该函数或复合语句执行结束退出时的该函数或复合语句的值。例如:
154
long f ( int n )
{ static long s ;
if ( n<=1 )
s=1 ;
else
s*=n ;
return s ;
}
main ( )
{
int i ; double k=0 ;
for ( i=1 ; i<=10 ; i++ )
k += 1.0 / f ( i ) ;
printf (,%lf,,k ) ;
}
计算,1+1/2 ! +1/3 ! + ……+ 1/10 !
S k
0 0
1 1
1*2 1*2
1*2*3 1*2*3
1*2*3*4 1*2*3*4.
...
..
/*计算并返回 n!的值 */
155
静态全局变量其作用域仅在声明该变量的源文件范围内的一种全局变量。
因此,这种变量对属于同一程序的其他源文件中的函数来说是不可见的,且在定义它的文件中,
也仅仅在其声明位置后面的那些函数能直接存取它们;而在其声明位置前面的那些函数若要使用它们,
可用将要介绍的 extern进行引用说明进行。
例如:
156
int fun1 (……)
{
……
}
static float buf = 1000 ;
double fun2 (……)
{
……
}
char fun3 (……)
{
……
}
源文件 file1.c的内容是:
main( )
{
extern float buf ;
extern char fun3(….) ;
int i ; double k=0 ;
for( i=1; i<=3 ; i++)
k+= buf / fun3 ;
printf(“%lf,,k ) ;
}
源文件 file2.c的内容是:
在 file1.c中说明要引用
file2.c中的
st tic型外部变量是错误的!
157
#define c 3
main ( )
{
static int a=1,b=c+2*sizeof(a+1.5) ;
}
在声明外部对象 (变量、数组等 ),static 型的外部对象,static 型的局部对象时,都可以指定初始化值,
但初始化值只能是是常量、常量表达式 ; 若定义时没有指定初始化值,则由编译系统自动置初值 0,而不管哪一种数据类型,它们都可以直接使用。例如:
注意,sizeof(a+1.5)
这个表达式是一个常量表达式,它是在编译阶段完成的。
158
extern 关键字的主要作用是用来 说明 要使用在程序中别的地方定义过的外部对象 (函数、变量、数组),这种说明的本质是 通过这种引用性说明来扩大指定 外部 对象的作用域。
声明 和 说明 的含义是不同的:
声明即指定义,一定对指定的外部对象分配存储空间 ;
说明只是指出需要引用源程序文件中别的地方已经声明过的外部对象,一定不会对被说明的外部对象分配存储空间。例如:
注意:
extern 关键字也可用 extern声明 外部变量和数组。
159
源文件 file1.c的内容是:
main( )
{ int f1(int x);
extern float v ;
extern double k[3],a ;
extern char f2( int y) ;
k[1]+=v/f1(1)*f2(2)/a;
printf("%lf ",k[1] ) ;
}
double a=5.0 ;
int f1 ( int x)
{
return x*x ;
}
源文件 file2.c的内容是:
extern double f3(int) ;
char f2 ( int y)
{
return f3(3)*y ;
}
float v=2.17 ;
double k[3];
double f3 ( int z)
{
return z+z;
}
引用 file2.c中定义的外部变量 v,外部数组 k、
函数 f2以及 file1.c中外部变量 a的 extern说明引用 file2.c中后面定义的 函数 f3的 extern
说明
160
用 extern 说明的对象一定是外部对象。不能引用局部对象。 例如:
main ( )
{
int f1(int x) ;
extern int k ;
int s=5;
k+=s*f1(2) ;
printf("%lf ",k ) ;
}
int f1 ( int x)
{
int k=2 ;
return x*x ;
}
错误 引用 函数 f1中定义的局部变量 k
161
若用 extern只说明引用函数,引用时无论是本文件中的函数、还是其他文件中的函数、在函数外说明、还是在函数内说明,都可以不写 extern关键字。
double f3(int) ;
char f2 ( int y)
{
return f3(3)*y ;
}
double v=2.17 ;
double k[3];
double f3 ( int z)
{
return z+z;
}
main( )
{ int f1(int x) ;
extern double k[3],v,a ;
char f2( int y) ;
k[1]+=v/f1(1)*f2(2)/a;
printf("%lf ",k[1] ) ;
}
double a=5.0 ;
int f1 ( int x)
{
return x*x ;
}
源文件 file1.c的内容是,源文件 file2.c的内容是:
引用 file2.c中后面定义的 函数 f3的无
extern关键字 说明引用 file1.c中后面定义的 函数 f1的无
extern关键字 说明引用 file2.c中定义的函数 f2的无 extern关键字 说明
162
因 static函数及 static外部变量,static外部数组只能在定义它们的源文件中的函数引用,所以不能在其他文件中用 extern来引用这些对象。
源文件 file2.c的内容是:
double f3(int) ;
static char f2 ( int y)
{
return f3(3)*y ;
}
static double v=2.17 ;
double k[3];
double f3 ( int z)
{
return z+z;
}
源文件 file1.c的内容是:
main( )
{ int f1(int x) ;
extern double k[3],v,a ;
char f2( int y) ;
k[1]+=v/f1(1)*f2(2)/a;
printf("%lf ",k[1] ) ;
}
double a=5.0 ;
int f1 ( int x)
{
return x*x ;
}
不能引用 file2.c中的 static型的外部变量和函数
163
在程序中,一个外部对象只能声明一次,但却可以用 extern 对同一个外部对象进行 多次引用 说明,
既可在函数外说明,也可在函数内说明。
int max ( int x,int y )
{ extern int a,b ;
int z ;
z = x>y? x,y ;
return ( z ) ;
}
extern int a,b ;
void main ( )
{
printf (,%d”,max ( a,b) ) ;
}
int a=13,b=-8 ; /* 外部变量定义 */
对外部变量 a、
b 的多次引用说明该 extern
引用说明仅对该函数有效!该 extern
引用说明对其后的所有函数有效!
164
若用 extern定义 一个外部变量、数组,定义时必须同时给出初始化值。相反,用 extern说明 一个变量则不能指定初始化值。
extern int x=2 ;
main ( )
{
x+=2 ;
printf("%d\n",x);
}
extern int x=2 ;
main ( )
{
x += 2 ;
printf ( "%d\n",x ) ;
}
int x ; /*外部变量定义 */
int f(int k)
{
sacnf(“%d”,&x);
}
这是 引用外部变量说明,不能指定初始化值。
用 extern定义外部变量,必须指定初始化值。
165
extern int x ;
main ( )
{
x = 2 ;
printf ( "%d\n",x ) ;
}
下面的例子说明程序中用 extern引用了没有定义的外部变量的错误(连接外部变量错误)。
Linker Error,Undefined symbol '_x' in module NONAME.C
引用了没有定义的外部变量说明。
166
用 extern 说明使用一个外部数组时,可以不给出数组大小,但 [ ]不能不写。
extern int x[ ],i,k,sum ;
main ( )
{ void f(int k) ;
f(5) ;
for ( k=0; k<5; k++ ) sum+=x[k] ;
printf ( "%d\n",sum ) ;
}
int x[5],i,k,sum ; /*外部对象定义 */
void f(int k)
{
for( i=0; i <k; i++)
scanf (,%d”,&x[i] ) ;
}
可以不给出数组大小
167
用 extern 说明或者声明一个变量、数组时,若说明或者声明的变量、数组的数据类型为为 int,则 int
可以省略不写。
extern x,y=10 ;
main ( )
{ int z ;
x=2 ;
z=x*y ;
printf ( "%d\n",z ) ;
}
int x ;
这儿省略了关键字
int
其中:
y是定义,
x是说明
168
extern 引用说明可以在函数内部,也可以在函数外部。但若在函数内部,如果引用的是变量、数组,则不能与该函数的局部变量、数组同名。
main ( )
{
extern int x ;
int x,z,y=10 ;
x=2 ;
z=x*y ;
printf ( "%d\n",z ) ;
}
int x ; /*外部变量定义 */
在这儿说明引用外部变量
x,将会导致同名错误。
但在没有同名的情况下是允许的!
Error C:\prog.C 5,Redeclaration of 'x' in function main
main ( )
{
int x,z,y=10 ;
x=2 ;
z=x*y ;
printf ( "%d\n",z ) ;
}
int x ;
extern int x ;
应在函数外部作引用说明。
169
int a ;
void main ( )
{
int power ( int n ) ;
int b=3,c,d,m ;
printf (,Enter a and m:” ) ;
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 ) ;
}
例,给定值 b,输入 a和 m,求 a*b 和 am 的值。
文件 file1.c中的内容为:
引用
file2.c中函数
power的说明外部变量 a定义
170
extern int a ;
int power ( int n )
{
int k,y=1 ;
for ( k=1 ; k<=n ; k++ )
y*=a ;
return ( y ) ;
}
文件 file2.c中的内容为:
程序运行时的输入 /出:
Enter a and m,3,3
3*3=9
3**3=27
说明 要使用
file1.c中的全局变量 a
171
8.10 内部函数和外部函数因为在一个函数中不能再定义另一个函数,所以函数总是一种外部全局对象,可被程序中的另外的函数调用。但也可以指定程序中某个函数不能被其他文件中的函数调用。 根据函数能否被其他源文件的函数调用而把函数分成为 内部函数 和 外部函数 。
1、内部函数如果一个函数 只能被定义它的源程序文件中的其他函数所调用,这样的函数就称为内部函数 。定义时,只要在函数名前加上 static 关键字即可。故内部函数又称 静态函数 。
172
由于 内部函数 的作用域只局限于其所在的文件,
对其他源文件中的函数来说是不存在、不可见、当然是不可调用的。因此,在同一个源程序的不同的源文件中可以定义同名的函数,不会引起冲突。
例如:
假定有一个 C 源程序由两个源程序文件组成,
文件名分别为发 f1.c 和 f2.c,它们的内容分别是:
173
f1.c文件的内容:
extern void f2(void) ;
void f1 (void) ;
main( )
{
f1( ) ;
f2( ) ;
}
void f1(void)
{
printf("call f1 in file1.c" ) ;
}
174
f2.c文件的内容,f1 函数是一个内部函数,只能由文件 f2.c中的函数调用。
它与文件 f1.c
中的函数 f1同名不会产生冲突!
void f1(void) ;
void f2(void)
{
f1( ) ;
}
static void f1(void)
{
printf("call f1 in file2.c" ) ;
}
175
2,外部函数所谓外部函数就是在定义函数时在函数头部的左端加上关键字 extern的函数。其实 定义函数时若不写 extern则隐含为 extern。 外部函数可由程序中所有其他函数调用。
例如:
extern int fun ( int a,int b )
{
……
}
176
输入一个字符,把已有的一个字符串中的该字符删去。程序由四个文件组成,每个文件包含一个函数。
源程序文件 file1.c 的内容:
例,运行多文件C语言源程序例
extern void enter_string ( char str[80] )
{
gets ( str ) ;
}
程序的功能是:
177
源程序文件 file2.c 的内容:
void delete_string (char str[ ],char ch )
{
int k,j ;
for ( k=j=0 ; str[k]!=?\0? ; k++ )
if ( str[k]!=ch )
str[j++]=str[k] ;
str[j]=?\0? ;
}
178
源程序文件 file3.c 的内容:
void print_string ( char str[ ] )
{
printf (,%s”,str ) ;
}
179
#include <stdio.h>
#include <string.h>
void main ( )
{
extern void enter_string ( char str[80] ) ;
extern void delete_string ( char str[ ],char ch ) ;
extern void print_string ( char str[ ] ) ;
char c,str[80] ;
enter_string ( str ) ;
scanf (,%c”,&c ) ;
delete_string ( str,c ) ;
print_string ( str ) ;
}
源程序文件 file4.c的内容:
180
abcdefgc↙ (输入字符串 )
c↙ (输入要删去的字符 )
abdefg (输出已删去指定字符后的字符串 )
程序运行例:
181
8.11 如何运行一个多文件的程序第一步 分别建立源程序文件 (例,f1.c,f2.c……)
第二步 建立 project文件(,prj文件 )
project文件内容为所有源程序文件名。如
f1.c,f2.c,…… ( 每一行一个文件名)
第三步 设置,Alt+p -> project -> project name ->
输入 project文件名(如 my.prj) -> 回车第四步 按 Ctrl+F9 进行编译、连接、运行。
第五步 回到正常状态。
Alt+p -> Clear project