第12章 编译预处理 12.1 了解编译预处理 12.2 宏 定 义 12.3 文 件 包 含 12.4 条 件 编 译
12.1 了解编译预处理 C语言所提供的“编译预处理”功能是它区别于其他高级语言的又一重要特征,其主要功能包括三种:宏定义、文件包含和条件编译,分别由三种命令来实现。为了区别于C语句,预处理命令必须出现在一行的开始并以“#”号打头。
12.2 宏 定 义 宏定义由#define命令实现,根据实际应用的需要可以分为不带参数的宏定义和带有参数的宏定义两种命令形式。 12.2.1 不带参数的宏定义 命令的一般形式为: #define 标识符 字符串
定义中的“标识符”称为“宏名”。这一命令的作用仅仅是表明用指定的宏名来代替一个指定的字符串。例如: #define PI 3 .1415926 它的作用是指定用宏名PI来代替“3 .1415926”这个字符串。在预编译时,程序中所有出现“宏名”的地方,都将由这个字符串的内容进行替换,此过程称为“宏展开”。
说明 (1)不带参数的宏定义常常被用于定义符号常量。这样既可以提高程序的通用性,也便于修改。 例12.1 用宏名限定数组的大小。 #define N 10 /* 定义宏名N */ void ArrIn(int a[]) { int i;
for(i=0; i<N; i++) /* 为N个元素赋值 */ scanf("%d",&a[i]); } main( ) { int x[N]; /* 定义有N个元素的数组 */ ArrIn(x); …
以上是我们在编程中常用的方法:首先利用宏定义指定用N代表字符串10;所以主函数中定义的数组大小是10;函数ArrIn中读入的元素个数也是10。当需要改变数组大小时,只需改动#define命令行,而不用改动程序的其他部分。 (2)宏展开时系统不作语法检查,若定义不当,要待编译展开源程序时,才会引发错误。
例如: #define PI 3.141592b main( ) { float s, r=2.5; s=PI*r*r; printf("s=%f\n",s); }
程序中在定义代表值的宏名PI时,将字符串3 程序中在定义代表值的宏名PI时,将字符串3.1415926中的数字6误写作了字母b。这一宏定义命令本身并不构成错误。因为宏名PI代表的是字符串“3.141592b”,而不是数值。但在程序中引用了宏名PI来计算圆面积,赋值语句: s=PI*r*r;
经宏展开后变为: s=3.141592b*r*r; 从而导致编译出错。虽然出错位置在此赋值语句,但引发这一错误的根源还是上述宏定义命令。
(3)宏展开时,不对双引号括起来的字符串中的内容进行替换。如有定义: #define PI 3.14159 语句:printf("PI=%f\n",PI);中有2个PI,第一个PI是在双引号内的,它不被替换;第二个PI在双引号外,它将被展开成3.14159。
(4)在#define命令行中可以没有“字符串”,即表示成: 如: #define DEBUG 这里仅仅是为了表明标识符DEBUG已经“被定义”。这种定义形式在条件编译中将会用到。
(5)为了与变量名相区别,一般习惯用大写字母作宏名。但这不是规定,只是约定俗成。
12.2.2 带参数的宏定义 1.定义形式 #define 宏名(形参表) 字符串 例如: 12.2.2 带参数的宏定义 1.定义形式 #define 宏名(形参表) 字符串 例如: #define CYC(r) 2*3.14159*r
说明 (1)这里定义了一个计算圆周长的宏CYC(r),宏名CYC与其后的左括号之间不得留有空格,否则CYC将被视为不带参数的宏。 (2)作为替换文本的字符串中应含有括号中的参数,如2*3.14159*r中的r。否则,就没有必要定义带参数的宏。
(3)字符串中可以包含已经定义过的宏名。例如: #define CR "\n" #define D "%d" #define PRI D CR 则语句:printf(PRI, 20);执行后的输出结果为:20。
2.宏展开的过程 程序中若调用了带参数的宏,预编译时不仅仅是进行简单的字符串替换,还涉及到参数的替换。宏展开的过程是:按命令行中指定的字符串从左到右进行置换,遇到形参则以实参代替,对于非参数字符则原样保留。若有语句:
x=CYC(2.5); 宏展开时将用实参2.5替换字符串“2*3.14159*r”中的形参r,其他字符不变。置换后的语句为: x=2*3.14159*2.5;
说明 (1)由于宏定义是在预编译时展开的,它不具有任何计算功能。宏展开时,若实参是一个表达式,也是原样置换,而不进行求值。如有语句: y=CYC(3+5);
这里用实参3+5替换“2*3.14159*r”中的形参r后得到: y=2*3.14159*3+5; 而不是: y=2*3.14159*8; /* 3和5不能先做加法运算 */
(2)显然,为确保宏调用的正确性,在定义宏时,应将字符串中的形参用括号括起来。例如: #define CYC(r) 2*3.14159*(r) 这时将语句y=CYC(3+5);展开后为: y=2*3.14159*(3+5); 有时还需要在整个字符串外面加括号。如对于以下定义的计算两数之和的宏:
#define ADD(x,y) x+y 若宏调用形式为: k=2*ADD(3,4); 展开后为: k=2*3+4; 显然与设计意图不符,这时宏定义应改写作: #define ADD(x,y) (x+y)
3.带参数的宏与函数的主要区别 通过前面的举例,读者可能会觉得:在定义宏时有形参,通过宏调用给出实参,这种形式与函数的使用十分相似。事实上,它们是有本质不同的。主要体现在以下几方面。
(1)函数调用是在程序执行过程中进行的,要占用系统运行时间;宏展开是在预编译时进行的,不占用系统运行时间。 (2)函数调用时,要为形参开辟临时单元,有值的传递和返回;宏定义和宏展开时都不需要开辟内存单元,也没有值的传递和返回。
(3)函数调用时,作为实参的表达式是经过计算求值后传给形参的,因此要求实参和形参类型一致;而宏展开只是进行相应的参数及字符替换,并不对实参求值,所以不存在类型问题。 (4)宏展开会使源程序代码增长;而函数调用不影响源程序。 读者可根据上述带参数的宏与函数的区别,决定实际应用时的选取。
12.3 文 件 包 含 文件包含由#include命令实现,前面的章节中已多次出现。这一命令的一般形式为: #include "文件名" 12.3 文 件 包 含 文件包含由#include命令实现,前面的章节中已多次出现。这一命令的一般形式为: #include "文件名" 或 #include <文件名> 命令行中的“文件名”应该是C语言的源文件。在预编译时,系统将用指定文件名中的全部内容来替换此命令行。
12.4 条 件 编 译 通常,一个源程序中的所有非注释语句都要通过编译而形成目标代码,而C语言允许有选择的、只对源程序的某一部分进行编译。这就是所谓的“条件编译”。