1、第4章 函数,返回总目录,目 录,4.1 函数调用过程,4.2 函数的定义,4.3 递归函数,4.6 习题四,4.4 存储类型、生存期和作用域,4.5 编译预处理,返回总目录,基本要求:)熟悉函数的定义、声明与调用,并熟悉递归函数的定义与使用;)熟悉存储类型、生存期和作用域;)熟悉编译预处理。 学习重点:)函数的定义与调用;)生存期、作用域、编译预处理。,4.1 函数调用过程,4.1.1 函数调用的定义,主函数main( )只能被系统调用,不能被其它函数所调用; 主函数main( )可以调用库函数或其他函数; 除主函数main( )外,其他函数之间可以相互调用; 在一个程序中,通过调用关系将各
2、函数联系在一起,程序总是从main( )函数开始执行,调用所需要的函数,完成所调用函数的功能,返回到main( )函数继续执行,最后在main( )函数中结束。,函数调用:在一个函数中引用另一个函数,称为,返回目录,4.1 函数调用过程,4.1.2 函数调用过程,函数调用的图示,设有main函数、fun函数,则它们的调用过程如图所示。,4.1 函数调用过程,4.1.2 函数调用过程,函数调用的图示,main() int a,b,c;scanf(“%d,%d“, ,4.1 函数调用过程,4.1.2 函数调用过程,首先为被调函数的所有形式参数分配内存,再计算实际参数的值,再一一对应地赋给相应的形式
3、参数(对于无参函数,不做此工作); 然后进入被调函数的函数体,为函数说明部分定义的变量分配存储空间,再依次执行函数体中的可执行语句; 当执行到“return”语句时,计算返回值(如果是无返回值的函数,不做这项工作);释放本函数中定义的变量所占用的存储空间(对于static类型变量,其空间不释放),返回主调函数继续执行。,函数调用过程简述,4.1 函数调用过程,4.1.2 函数调用过程,函数调用的规则说明,被调用的函数必须是已经存在的函数(即是库函数或用户自己定义的函数); 如果调用库函数,需要在程序的开头包含相应的头文件,如使用数学库中的函数,就用#include; 函数的声明:详见4.2.3
4、小节。,4.1 函数调用过程,4.1.2 函数调用过程,#include int max(int x,int y) int z;if(xy)z=x;else z=y;return z; main( ) int a,b,c;scanf(“%d,%d”,&a,&b);c = max(a,b);printf(“Max=%dn”,c); ,例4.1 从键盘输入2个整数,求较大的整数。,4.2 函数的定义,4.2.1 函数定义的一般形式,函数定义的一般形式为:函数类型说明 函数名(形参说明表) 说明部分;执行部分; 注:“函数类型说明”就是说明函数返回值的数据类型;“形参说明表”是对形参变量数据类型的说
5、明;花括号括起来的语句序列是函数体,它包括函数内部定义的变量说明和函数执行部分。,返回目录,4.2 函数的定义,4.2.2 函数定义的要点,函数类型说明,函数类型就是函数返回值的数据类型。函数返回时可能得到0个数据、1个数据或多个数据。函数类型的定义要根据函数是否有无返回值来定义。,4.2 函数的定义,4.2.2 函数定义的要点,函数类型说明,(1) 函数无返回值如果函数没有返回值,则一般在定义函数时把“函数类型说明符”说明为void。例如:void PRINT( )printf(“Testn“);,4.2 函数的定义,4.2.2 函数定义的要点,函数类型说明,(2) 函数有1个返回值这时在被
6、调用函数的函数体中有return语句。此时函数类型的定义应根据return语句后的表达式的数据类型来定义。例如:int max(int a,int b)return ab?a:b;,4.2 函数的定义,4.2.2 函数定义的要点,函数类型说明,(2) 函数有1个返回值当函数类型与return语句后的表达式的数据类型不一致时,函数类型决定return语句后的表达式的数据类型,系统自动将表达式的数据类型转换成函数定义的数据类型,在函数定义过程中,最好定义两者一致,以免结果出错; int max(float a,float b) return ab?a:b; ,main( ) printf(“%d“
7、,max(12.3,20.6); ,输出结果:20,4.2 函数的定义,4.2.2 函数定义的要点,函数类型说明,(2) 函数有1个返回值当缺省函数类型定义时,系统默认函数类型为int或char,同时也说明当函数类型为int或char时,可缺省函数类型说明。 max(int a, int b)return ab?a:b;,4.2 函数的定义,4.2.2 函数定义的要点,函数类型说明,(3) 函数有多个返回值这时在被调函数的函数体中一般无return语句。多个值的返回是通过全局变量、数组或指针作参数来实现的,这将在以后的章节中进行介绍。,4.2 函数的定义,4.2.2 函数定义的要点,形参表及形
8、参说明,在函数定义中写在圆括号中的参数称为形式参数。不带参数的函数其形参表是空的。一般无参函数在圆括号中可以写上“void”以取代空的形参表。例如:void PRINT( void)printf(“This is a examplen”); 有参函数的形参表由一个或多个形参组成,各参数间用逗号分隔。如果有形参,则必须定义形参的数据类型。形参类型有两种定义格式:,4.2 函数的定义,4.2.2 函数定义的要点,形参表及形参说明, 传统风格 函数类型 函数名(形参表) 形参类型; 函数体; 例如: int max(a,b) int a,b; , 现代风格 函数类型 函数名(形参表及类型) 函数体;
9、 例如: int max(int a, int b) ,4.2 函数的定义,4.2.2 函数定义的要点,参数值的传递,函数间通过参数传递数据,是通过调用函数中的实在参数(简称实参)向被调用函数中的形式参数(简称形参)传递进行的。实参向形参传递数据的方式:是实参将值单向传递给形参,形参值的变化不影响实参值。,4.2 函数的定义,4.2.2 函数定义的要点,例如: void swap( int x,int y) int z;z=x;x=y;y=z; main( ) int a=5,b=10;swap(a,b);printf(“a=%d,b=dn”,a,b); ,程序执行后,运行结果为: a=5,b
10、=10 而不是: a=10,b=5,参数值的传递,4.2 函数的定义,4.2.2 函数定义的要点,参数值的传递,定义函数时,定义的形参并不占用实际的存储单元,只有在被调用时才由系统给它分配存储单元,在调用结束后,形参所占用的存储单元被释放; 实参的个数与类型应与形参一致,否则将会出现编译错; C语言规定,函数间的参数传递是“值传递”,即值的单向传递,实参可以把值传给形参,但形参的值不能传给实参,也就是对形参的修改是不会影响到对应的实参; 数组名作为参数传递的是数组首地址,严格说,其传递的也是“值(地址值)”,指针变量作参数同此。(后面详讲),4.2 函数的定义,4.2.2 函数定义的要点,函数
11、体说明,函数类型说明 函数名(形参说明表) 说明部分;执行部分; ,说明部分是对函数中要用到的变量、要调用到的函数以及要引用的外部变量进行说明。,执行部分是用来实现函数的功能,即语句执行部分。,函数体,4.2 函数的定义,4.2.2 函数定义的要点,函数体说明,被调用函数的函数体中无return语句,或return语句不带表达式并不表示没有返回值,而是表示返回一个不确定的值。如果不希望有返回值,必须在定义函数时把“函数类型说明符”说明为void。 函数有返回值时,函数体中至少有一条return语句,执行到哪一个返回语句,该返回语句就起作用。但通过它只能得到一个数据,多个数据的返回不能通过ret
12、urn语句实现。,4.2 函数的定义,4.2.2 函数定义的要点,函数体说明,printstar() printf(“*“); main() int a;a=printstar();printf(“a=%d“,a); ,输出:a=10(不确定的值),int max(int a,int b) if(ab) return a;else return b; main() int x=4,y=5,z;z=max(x,y);printf(“max=%d“,z); ,多个return语句,但只1个被执行,4.2 函数的定义,4.2.2 函数定义的要点,例4.2 编程求2个正整数的最大公因子,int fun
13、(int m, int n) int r;r=m%n;while( r )m=n;n=r;r=m%n;return n; ,#include main( ) int x,y,z;scanf(“%d,%d“, ,例如输入:32,48, 则输出:z=16 (教材上多一条if语句实现交换,结果一样),4.2 函数的定义,4.2.3 函数的声明,函数已定义,如果要调用,一般应在主调函数中对被调函数进行声明,即向编译系统声明将要调用此函数,并将有关信息(如被调用函数名、函数类型、形参的个数及类型等)通知编译系统。,函数声明的含义,4.2 函数的定义,4.2.3 函数的声明,函数声明的一般形式,(1)函数
14、类型 函数名(形参类型1 参数1,形参类型2 参数2, );,(2)函数类型 函数名(形参类型1,形参类型2, );,如:int max(int a, int b);,如:int max(int, int);,因编译系统不检查参数名,故格式(1)(2)效果一样,例4.3 求两个数之和。 #include main( ) float fun(float x,float y);printf(“Sum=%fn”,fun(2,5); float fun(float x,float y) return x+y; ,4.2 函数的定义,4.2.3 函数的声明,函数声明的一般形式,例4.3 求两个数之和。
15、#include main( ) float fun(float,float);printf(“Sum=%fn”,fun(2,5); float fun(float x,float y) return x+y; ,Sum=7.000000,Sum=7.000000,例4.3 求两个数之和。 #include main( ) float fun( );printf(“Sum=%fn”,fun(2,5); float fun(float x,float y) return x+y; ,4.2 函数的定义,4.2.3 函数的声明,函数声明的一般形式,例4.3 求两个数之和。 #include mai
16、n( ) printf(“Sum=%fn”,fun(2,5); float fun(float x,float y) return x+y; ,Sum=0.000000,Error: Type mismatch,例4.3 求两个数之和。 #include float fun(float x,float y) return x+y; main( ) printf(“Sum=%fn”,fun(2,5); ,4.2 函数的定义,4.2.3 函数的声明,函数声明的一般形式,Sum=7.000000,对于用户自定义函数,若被调用函数定义在主调函数之前,可缺省声明。,示例 求两个数之和。 #include
17、 main( ) printf(“Sum=%dn“,fun(2,5); int fun(int x,int y) return x+y; ,4.2 函数的定义,4.2.3 函数的声明,函数声明的一般形式,Sum=7,对于用户自定义函数,若被调函数返回值是int或char时,则函数声明可缺省。,示例 求两个数之和。 #include main( ) printf(“Sum=%dn“,fun(2.7,5); int fun(int x,int y) return x+y; ,4.2 函数的定义,4.2.3 函数的声明,函数声明的一般形式,Sum=13107,对于用户自定义函数,若被调函数返回值是i
18、nt或char时,则函数声明可缺省。但缺省后,系统无法对参数类型作检查,若调用函数时参数使用不当,编译不报错,但执行结果会出错。,4.2 函数的定义,4.2.3 函数的声明,关于函数声明的小结,在今后的编程实践中,养成如下习惯就不会出现意想不到的错误。 将自定义函数在主调函数之前进行定义,则可缺省函数声明。 若自定义函数在主调函数之后进行定义,则严格按照两种声明格式在调用前进行函数声明。,4.3 递归函数,4.3.1 递归的概念,一个函数在它的函数体内直接或间接的调用它自身,则该函数称之为递归函数。 直接递归:函数直接调用自已。(常用:本节重点讨论)间接递归:函数间接调用自己。(不常用,本节不
19、讨论),返回目录,4.3 递归函数,4.3.1 递归的概念,例4.5 用递归法求f(n)=n!计算阶乘可用如下形式描述:,#include unsigned long fact(int n) unsigned long p;if(n= =1) p=1;else p=n*fact(n-1);return p; main( ) int n;unsigned long p;scanf(“%d”,&n);p=fact(n);printf(“%d!=%lun”,n,p); ,4.3 递归函数,4.3.1 递归的概念,p=3*fact(2)return p,p=2*fact(1)return p,p=1r
20、eturn p,fact(3)输出fact(3),fact(3),fact(2),fact(1),main( ),fact(3)=6,fact(2)=2,fact(1)=1,1,2,3,4,5,6,7,8,9,10,例4.5 用递归法求f(n)=n!若程序中输入的n值为3,则在main( )函数中调用了fact(3),其调用过程如下:,思考:程序中若没有if(n=1) p=1;语句,调用过程会怎样?,4.3 递归函数,4.3.1 递归的概念,一般形式1:返值类型 递归函数名fun(参数说明表)if(递归终止条件) 返回值p = 递归终止值; /*递归终止*/else 返回值p = 递归调用fu
21、n()的表达式;/*递归调用*/return p;,定义递归函数的一般形式,4.3 递归函数,4.3.1 递归的概念,一般形式2:返值类型 递归函数名fun(参数说明表)if(递归终止条件) return 递归终止值; /*递归终止*/else return 递归调用fun()的表达式;/*递归调用*/,定义递归函数的一般形式,4.3 递归函数,4.3.2 递归举例,附例:阶乘函数非递归形式 递归形式,int fact( int n ) if( n = 0 ) /*递归终止条件*/return 1; /*递归终止值*/else return( n * fact(n-1); /*递归调用*/ ,
22、附例:斐波那契(Fibonacci)数列非递归形式 递归形式,int fib( int n ) if(n=0 | n=1) /*递归终止条件*/return n; /*递归终止值*/ else return(fib(n-1)+fib(n-2); /*递归调用*/ ,4.3 递归函数,4.3.2 递归举例,4.3 递归函数,圆盘移动规则1)每次只能移动一个圆盘;2)圆盘可以插在X,Y和Z中的任一塔座上;3)任何时刻都不能将一个较大的圆盘压在较小的圆盘上。,例4.7 n阶Hanoi塔问题。,3阶Hanoi塔问题的初始与结果状态,4.3.2 递归举例,4.3 递归函数,(1)从A移动1n-1号圆盘至
23、B,C作辅助;(递归调用),分析此问题可归之于三个子问题,(2)从A移动n号圆盘至C; (一次搬运),(3)从B移动1n-1号圆盘至C,A作辅助。(递归调用),A,B,C,1n-1,n,4.3.2 递归举例,4.3 递归函数,void move(int n,char a,char b,char c) if( n0 ) move(n-1,a,c,b); printf(“%c-%cn”,a,c); move(n-1,b,a,c); ,解答求解n阶Hanoi塔问题的递归算法。,(1)从A移动1n-1号圆盘至B,C作辅助;(递归调用),(2)从A移动n号圆盘至C; (一次搬运),(3)从B移动1n-1
24、号圆盘至C,A作辅助。(递归调用),4.3.2 递归举例,4.4 存储类型、生存期和作用域,4.4.1 存储类型,定义一个变量的一般形式为:存储类型 数据类型 变量标识符表;,变量的数据类型规定了变量的存储空间大小和取值范围;(已介绍) 变量的存储类型规定了变量的生存期和作用域。变量的存储类型有4个,分别是自动型、寄存器型、外部型和静态型,其说明符分别是auto、register、extern和static。,返回目录,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,生存期,就是变量存活的周期。为了节省内存,避免变量互相干扰,不可能让所有的变量始终存在。有两种情况: 一是变量从程
25、序开始定义就被分配内存(即产生),直到程序运行结束内存释放,这种变量称为静态变量; 二是变量带有临时性,随调用的模块(如文件、函数或复合语句)分配内存单元,模块调用结束,释放内存,这种变量称为动态变量。,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,所谓变量的作用域,就是某个变量在其生命期内,程序模块是否可使用该变量。有两种情况: 一种变量在从定义点开始或整个源程序都可有效,称为全局变量; 另一种变量只能在所定义的模块内部有效,称为局部变量。,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,局部变量,局部变量定义在函数内部。 不同函数中可以使用相同名字的变量,它们
26、代表不同的对象,互不干扰。 函数的形参也是局部变量,只在所在函数中有效。 在函数内部复合语句中定义的变量,只在该复合语句中有效,出了复合语句就无效。,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,全局变量,全局变量定义在函数之外。 全局变量的有效范围是从定义的位置开始到整个程序的结束。 局部变量可以和全局变量同名,若同名,则在局部变量的作用域内,全局变量被“屏蔽”(即不起作用)。 全局变量的作用降低了函数的通用性,建议尽量少用或不用。,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,auto存储类型变量,在各个函数内部或复合语句内定义的变量。 自动变量用“auto
27、”进行标识,可缺省。例如,在函数体中,定义auto int b,c=3;与定义int b,c=3;是等价的。 自动变量的生存期:自定义开始,直到所在函数或复合语句结束。 自动变量的作用域:就是在定义该变量的函数或复合语句内。,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,auto存储类型变量,#include add(int x) x+;printf(“%d,”,x); sub( int x) x- -;printf(“%d,”,x); main( ) int y=1;add(y); sub(y);add(y); sub(y); ,程序运行的结果为: 2,0,2,0,例4.8
28、auto型变量特性的分析。,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,auto存储类型变量,#include main( ) int x=1,y=2;int x= 10;x=x+10; printf(“x=%d,”,x); printf(“y=%d,”,y); x=x+9;printf(“x=%dn”,x); ,例4.9 auto型变量作用域示例。,程序运行结果为:x=20,y=2,x=10,外层模块变量x的作用域被内层模块同名变量的作用域屏蔽,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,register存储类型变量,如果有一些变量使用频繁(例如在一个函数
29、中执行1000000次循环,每次循环中都要引用某局部变量),则要为存取该变量的值花不少时间。为提高执行效率,C语言允许将局部变量的值放在CPU中的寄存器中,需要用时直接从寄存器中取出参加运算。由于对寄存器的存取速度远远高于对内存的读取速度,因此这样可以提高执行效率。这种变量称为“寄存器变量”,用关键字register声明。,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,register存储类型变量,例如下面的程序段:long i, sum=0;for(i=1;i=1000000;i+)sum=sum+i; 可改成:register long i, sum=0;for(i=1;i
30、=1000000;i+)sum=sum+i; 说明:当今的优化编译系统能够识别使用频繁的变量,从而自动地将这些变量放在寄存器中,而不需要程序设计者指定。因此在实际上用register声明变量是不必要的。读者对它有一定了解即可。,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,外部变量,定义在所有函数(包括主函数)外部的变量称为外部变量。 对于外部变量,系统在编译时是将外部变量的内存单元分配在静态数据存储区,在整个程序文件运行结束后系统才收回其存储单元。因此外部变量的生存期:是从外部变量定义点开始,直至源程序运行结束。 作用域:是从定义点开始,直到程序结束。在定义点之前或别的源程序
31、中要引用外部变量,则在引用前,需进行声明(作用域扩展)。,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,外部变量,例4.10 用函数实现,求ax2+bx+c=0(a0)根。 解题思路 在a0的情况下,根据b*b-4*a*c的不同值,方程分别有两个不等的实根、两个相等的实根和两个共轭的虚根。如果传递方程的3个系数a、b、c给函数,分别求出方程的根。上述三种情况都需要返回两个数据,通过return语句无法实现,因此可借用外部变量实现。 程序如下:,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,外部变量,#include #include float x1,x2; /
32、*定义外部变量*/ void f1(float a,float b,float c) x1=(-b+sqrt(b*b-4*a*c)/(2*a);x2=(-b-sqrt(b*b-4*a*c)/(2*a); void f2(float a,float b) x1=x2=-b/(2*a); void f3(float a,float b,float c) x1=-b/(2*a);x2=sqrt(4*a*c-b*b)/(2*a); ,main( ) float a,b,c;scanf(“%f%f%f”, ,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,外部变量,例4.11 外部变量引用
33、示例 #include int x=10; /*外部变量x的定义*/ void f1( ) x+;printf(“x=%d,”,x);printf(“y=%fn”,y); float y=2;/*外部变量y的定义*/ main( ) int x=1;x+;f1( );printf(“x=%d,y=%fn”,x,y); ,编译结果: Error: y : undeclared identifier,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,外部变量,例4.11 外部变量引用示例 #include int x=10; /*外部变量x的定义*/ void f1( ) extern
34、 float y; /*外部变量声明,扩展了外部变量y的作用域*/x+;printf(“x=%d,”,x);printf(“y=%fn”,y); float y=2;/*外部变量y的定义*/ main( ) int x=1;x+;f1( );printf(“x=%d,y=%fn”,x,y); ,运行结果: x=11,y=2.000000 x=2,y=2.000000,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,静态变量,静态变量分静态局部变量和静态全局变量。 (1) 静态局部变量 静态局部变量是定义在函数体的复合语句中,用关键字“static”进行标识的变量。静态局部变量定义
35、的一般形式为:static 变量数据类型 变量名表;,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,静态变量,对于静态局部变量,系统在编译时将其内存单元分配在静态数据存储区,并在编译时赋初值(即只赋初值一次),在程序运行时它已有初值,以后再次使用时不必重新分配内存单元和赋初值,其值是保留上一次函数调用结束时的值。直到程序运行结束,对应的内存单元才释放。 生存期:从变量定义开始,直到程序运行结束。 作用域:在所定义的函数或复合语句中有效。 因此静态局部变量是一种具有全局寿命、局部可见的变量。,例4.12 静态局部变量示例。 #include void f1( ) int x=1;
36、x+;printf(“x=%d,“,x); void f2( ) static int x=1;x+;printf(“x=%dn“,x); ,main( ) f1( ); f2( );f1( ); f2( ); 程序运行结果为: x=2,x=2 x=2,x=3,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,静态变量,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,静态变量,例4.13 利用静态局部变量求 10!,程序如下: #include long int fun(int n) static long int s=1;s=s*n;return s; main(
37、) int n;long int p;for(n=1;n=10;n+)p=fun(n);printf(“%ldn”,p); ,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,静态变量,(2) 静态全局变量 定义在所有函数(包括主函数)之外,用关键字“static”标识的变量,称为静态全局变量。 生存期:静态全局变量和外部变量都具有全局寿命,即在整个程序运行期间都存在。 作用域:静态全局变量只能在所定义的文件中使用,具有局部可见性。这与外部变量不同。 注意:自动变量没有赋初值时,其值是一个随机值。对于静态变量或外部变量没有赋初值时,数值型变量的值系统默认为0。,4.4 存储类型、生
38、存期和作用域,4.4.2 生存期和作用域,静态变量,例4.14 静态全局变量与外部变量的区别。 第一个文件的内容如下: /*file1.c*/ #include int a=2; static int b=3; func( ) a+; b+;printf(“a=%d,b=%dn“,a,b); ,第二个文件的内容如下: /*file2.c*/ #include extern int a; int b; main( ) func( );printf(“a=%d, b=%dn“,a,b); ,4.4 存储类型、生存期和作用域,4.4.2 生存期和作用域,静态变量,将两文件置于同一工程中。程序运行结果
39、如下:a=3,b=4a=3,b=0 从上述运行结果可以看出,文件file1.c中的外部变量a可以在文件file2.c中使用,只需在file2.c中加上声明语句:extern int a;即可.,Linker Error: error LNK2001: unresolved external symbol _b,但file1.c中的静态全局变量b不能在file2.c中使用。读者可将file2.c中变量定义语句:int b;改写成变量声明语句:extern int b; 想一想,会出现什么错误?为什么?,4.4 存储类型、生存期和作用域,4.4.3 存储类型小结,按作用域分类:局部变量和全局变量,
40、4.4 存储类型、生存期和作用域,4.4.3 存储类型小结,4.4 存储类型、生存期和作用域,4.4.3 存储类型小结,目前我们在一般的编程实践中,绝大部分涉及到的都是“自动变量”(auto),偶尔会涉及到“静态局部变量”(static)或“外部变量”(extern)。而“自动变量”(auto)在书写时可以缺省,所以,变量定义时一般不必考虑存储类型,但要重点理解“自动变量”的作用域与生存期。,4.5 编译预处理,4.5.0 引言,编译预处理是在对源程序进行正式编译之前的处理。 这些预处理命令是ANSI C统一规定的,不能直接对它们进行编译(包括词法和语法分析、代码生成、优化等),而必须在编译之
41、前,先对程序中这些特殊命令进行“预处理”,然后再由编译程序对预处理后的源程序进行正常的编译处理。,返回目录,4.5 编译预处理,4.5.0 引言,编译预处理是在对源程序进行正式编译之前的处理。 例1:若程序中用#define命令定义了一个符号常量A(#define A 100),则在预处理时将程序中所有A都置换为指定的值(100)。 例2:若程序中用#include命令包含了一个文件“stdio.h”(#include,则在预处理时将stdio.h文件中的实际内容代替该命令,4.5 编译预处理,4.5.0 引言,C提供的预处理功能主要有以下三种: 1)文件包含 2)宏定义 3)条件编译 为了与
42、一般C语句相区别,这些命令前以符号“”开头。并占用一个单独的书写行,语句结尾不加分号。,4.5 编译预处理,4.5.1 文件包含,文件包含命令的作用,如果文件A中有一条文件包含预处理命令:#include 该命令将指定文件B的内容加到文件A中“#include ”命令处的位置,共同组成一个程序文件,即在文件A中产生文件B的一个副本。 例如: #include /*把含有输入/输出标准函数的头文件包含到当前源程序文件中*/ #include /*把含有标准数学函数的头文件包含到当前源程序文件中*/,4.5 编译预处理,4.5.1 文件包含,文件包含命令的作用,图4.6是一个文件包含关系的示意图。
43、文件file1.c中的包含命令#include “file2.c“将文件file2.c包含进文件file1.c。图4.6(a)和图4.6(b)是预处理前的情况,图4.6(c)是将文件file2.c包含进文件file1.c之后的file1.c结构示意图。,图4.6 文件包含示意图,4.5 编译预处理,4.5.1 文件包含,#include命令的两种形式,格式一:#include 本命令的特点是在文件名的首尾用尖括号括起来。文件名可以带路径。在预处理时将只在所指定的标准目录(即C系统安装后形成的include子目录,该子目录中有系统提供的头文件)中查找包含文件。,4.5 编译预处理,4.5.1 文
44、件包含,#include命令的两种形式,格式二:#include “文件名“本命令格式的特点是在文件名的首尾用双引号括起来。其中,文件名指出的是待包含进来的文件,且可以带路径。若文件名带路径,则预处理时在指定的路径下去查找。若查找不到,再到系统指定的标准目录中找。找到文件后,用文件内容替换该命令。因此格式二的查找功能包含了格式一的查找功能。另外,两种格式的include后可不带空格。,4.5 编译预处理,4.5.1 文件包含,例4.14 设计一个求n!的函数,存放于文件exam.c中,然后设计主函数文件file.c,计算p=n!/m!/(n-m)! 可以编写文件exam.c如下: /*exam
45、.c*/ long int fact(int n) long int p=1;int k;for(k=1;k=n;k+)p=p*k;return p; ,另一文件file.c的内容如下: /*file.c*/ #include #include “exam.c“ main( ) int m,n;long int p;printf“Input n,m(nm):”);scanf(“%d,%d”,&n,&m);p=fact(n)/fact(m)/fact(n-m);printf(“ldn”,p); ,4.5 编译预处理,4.5.2 宏定义,宏定义命令的作用,宏定义的作用:用标识符来代表一串字符。一旦
46、对字符串命名,就可在源程序中使用宏定义标识符,系统编译之前会自动将标识符替换成字符串。宏定义有两种,即不带参数的宏和带参数的宏。,4.5 编译预处理,4.5.2 宏定义,1. 不带参的宏,不带参数的宏定义的一般形式为:#define 宏名 宏体 其中,“#”表示这是一条预处理命令,“define”为宏定义命令;“宏名”为一个合法的标识符,一般建议用大写字母,以与变量名相区别;“宏体”可以是常数、表达式或语句,甚至可以是多条语句。三者之间要用一个或多个空格分隔。 例如:#define PI 3.1415926#define MSG printf(“Hello,”);printf(“world!n
47、”); 在预处理时,系统在该宏定义以后出现的每一个宏名都用宏体来代替,这个过程叫宏替换。,4.5 编译预处理,4.5.2 宏定义,1. 不带参的宏,例4.16 求球的表面积和体积。 #include #define PI 3.1415926 main( ) float s,v,r;scanf(“%f”, 在预处理时,系统会将main函数中的PI自动用3.1415926替换。上述文件将变为:,#include main( ) float s,v,r;scanf(“%f”,&r);s=4*3.1415926*r*r;v=4*3.1415926*r*r*r/3;printf(“Area=%f, Vo
48、lume=%fn”, s, v): 采用宏定义后,如果要修改PI值,只需在宏定义处修改PI值,而不需在计算面积和体积等多处修改,减少了代码修改量,降低了程序出错的可能性,提高了程序的健壮性。,4.5 编译预处理,4.5.2 宏定义,1. 不带参的宏,附例:宏体可以是多条语句 #include #define MSG printf(“Hello,“);printf(“world!n“); main( ) MSG ,Hello,world!,程序运行结果:,4.5 编译预处理,4.5.2 宏定义,2. 带参的宏,带参数宏定义的一般形式为:#define 宏名(参数) 宏体 例如: #define SQR(x) (x)*(x)#define MOD(x,y) (x)%(y) 预处理时,系统在用宏体代替宏名的同时,实在参数会代替宏体中形式参数,同样宏替换仍只是一种简单的替换,不能进行计算或其它的功能。 当程序出现下列语句时:y=SQR(a+b); 程序在预处理时,将被替换成如下语句:y=(a+b)*(a+b);,4.5 编译预处理,4.5.2 宏定义,2. 带参的宏,注意:在定义带参数的宏时,宏体中的所有形式参数和整个表达式最好都加圆括号,否则在宏替换后的表达式中,运算顺序可能会发生不希望的变化。,