9.1 宏定义
9.2,文件包含”处理
9.3 条件编译习题第 9章 预处理命令
ANSI C标准规定可以在 C源程序中加入一些“预处理命令” (preprocessor DireCtiveS),以改进程序设计环境,提高编程效率。这些预处理命令是由
ANSI C统一规定的,但是它不是 C语言本身的组成部分,不能直接对它们进行编译 (因为编译程序不能识别它们 )。必须在对程序进行通常的编译 (包括词法和语法分析、代码生成、优化等 )之前,先对程序中这些特殊的命令进行“预处理”,即根据预处理命令对程序作相应的处理 (例如,若程序中用 #define命令定义了一个符号常量 A,则在预处理时将程序中所有的 A都置换为指定的字符串。
若程序中用 #inClude命令包含一个文件,StDio.h”,
则在预处理时将 StDio.h文件中的实际内容代替该命令 )。经过预处理后程序不再包括预处理命令了,最后再由编译程序对预处理后的源程序进行通常的编译处理,得到可供执行的目标代码。现在使用的许多 C编译系统都包括了预处理、编译和连接等部分,
在进行编译时一气呵成。因此不少用户误认为预处理命令是 C语言的一部分,甚至以为它们是 C语句,
这是不对的。必须正确区别预处理命令和 C语句、
区别预处理和编译,才能正确使用预处理命令。 C
语言与其他高级语言的一个重要区别是可以使用预处理命令和具有预处理的功能。
C提供的预处理功能主要有以下三种:
1,宏定义
2,文件包含
3,条件编译分别用宏定义命令、文件包含命令、条件编译命令来实现。为了与一般 C语句相区别,这些命令以符号,#”开头。
9.1 宏定义
9.1.1 不带参数的宏定义用一个指定的标识符 (即名字 )来代表一个字符串,
它的一般形式为 #define 标识符字符串。这就是已经介绍过的定义符号常量。
如,#definePI3.1415926它的作用是指定用标识符
PI来代替,3.1415926”这个字符串,在编译预处理时,将程序中在该命令以后出现的所有的 PI都用,3.1415926”代替。这种方法使用户能以一个简单的名字代替一个长的字符串,因此把这个标识符 (名字 )称为“宏名”,在预编译时将宏名替换成字符串的过程称为“宏展开”。 #define是宏定义命令。
例 9.1
#define PI 3.1415926
main()
{ float l,s,r,v;
printf("input raDiuS∶ ");
scanf("%f",&r);
l=2.0*PI*r;
s=PI*r*r;
v=3.0/4*PI*r*r*r;
printf("l=%10.4f\nS=%10.4f\nv=%10.4f\n",l,s,
v);

运行情况如下:
input radius,4
l=25.1328
s=50.2655
v=150.7966
说明:
(1) 宏名一般习惯用大写字母表示,以便与变量名相区别。但这并非规定,也可用小写字母。
(2) 使用宏名代替一个字符串,可以减少程序中重复书写某些字符串的工作量。例如,如果不定义 PI代表 3.1415926,则在程序中要多处出现 3.1415926,不仅麻烦,而且容易写错 (或敲错 ),用宏名代替,
简单不易出错,因为记住一个宏名 (它的名字往往用容易理解的单词表示 )要比记住一个无规律的字符串容易,而且在读程序时能立即知道它的含义,
当需要改变某一个常量时,可以只改变 #define命令行,一改全改。例如,定义数组大小,可以用
#define array- size 1000
int array[array-size];
先指定 array-size代表常量 1000,因此数组 array大小为 1000,如果需要改变数组大小,只需改 #define
行:
#define array-size 500
使用宏定义,可以提高程序的通用性。
(3) 宏定义是用宏名代替一个字符串,也就是作简单的置换,不作正确性检查。如果写成
#define PI 3.l4l59
即把数字 1写成小写字母 l,预处理时也照样代入,
不管含义是否正确。也就是说预编译时不作任何语法检查。只有在编译已被宏展开后的源程序时才会发现错误并报错。
(4) 宏定义不是 C语句,不必在行末加分号。如果加了分号则会连分号一起进行置换。如:
#define PI 3.1415926;
area=PI*r*r;
经过宏展开后,该语句为
area=3.1415926;*r*r;
显然出现语法错误。
(5) #define命令出现在程序中函数的外面,宏名的有效范围为定义命令之后到本源文件结束。通常,
#define命令写在文件开头,函数之前,作为文件一部分,在此文件范围内有效。
(6) 可以用 #undef命令终止宏定义的作用域。例如:
#define g 8.8
main()
{
| G的有效范围

}
#undef g
f1()
{

}
由于 #undef的作用,使 G的作用范围在 #undef行处终止,因此在 f1函数中,g不再代表 8.8。这样可以灵活控制宏定义的作用范围。
(7) 在进行宏定义时,可以引用已定义的宏名,可以层层置换。
例 9.2
#define R 3.0
#define PI 3.1415926
#define L 2*PI*R
#define S PI*R*R
main()

printf("L=%f\ns=%f\n",L,S);

运行情况如下:
L=18.849556
s=28.274333
经过宏展开后,printf函数中的输出项 L被展开为
2*3.1415926*3.0,S展开为 3.1415926*3.0*3.0,
printf函数调用语句展开为
printf("L=%f\ns=%f\n",2*3.1415926*3.0,
3.1415926*3.0*3.0);
(8) 对程序中用双括号括起来的字符串内的字符,即使与宏名相同,也不进行置换。如例 9.2中的
printf函数内有两个 l字符,一个在双引号内,它不被宏置换,另一个在双引号外,被宏置换展开。
(9) 宏定义是专门用于预处理命令的一个专用名词,
它与定义变量的含义不同,只作字符替换,不分配内存空间。
9.1.2 带参数的宏定义不是进行简单的字符串替换,还要进行参数替换。
其定义的一般形式为
#define 宏名 (参数表 ) 字符串字符串中包含在括弧中所指定的参数。如:
#define s(a,b) a*b
area=s(3,2);
定义矩形面积 S,a和 B是边长。在程序中用了 s(3,
2),把 3,2分别代替宏定义中的形式图 9.1
参数 a,b,即用 3*2代替 s(3,2)。因此赋值语句展开为 area=3*2;
对带参的宏定义是这样展开置换的:在程序中如果有带实参的宏 (如 S(3,2)),则按 #define命令行中指定的字符串从左到右进行置换。如果串中包含宏中的形参 (如 a,b),则将程序语句中相应的实参 (可以是常量、变量或表达式 )代替形参,如果宏定义中的字符串中的字符不是参数字符 (如 a*b中的 *号 ),则保留。这样就形成了置换的字符串,
见图 9.1。
例 9.3
#definePI3.1415926
#define S(r) PI*r*r
main()
{ float a,area;
a=3.6;
area=S(a);
printf("r=%f\narea=%f\n",a,area);

运行结果如下:
r=3.600000
area=40.715038
赋值语句 area=s(a);经宏展开后为
area=3.1415926*a*a;
说明:
(1) 对带参数的宏的展开只是将语句中的宏名后面括号内的实参字符串代替 #define命令行中的形参。例
9.3中语句中有 S(a),在展开时,找到 #define命令行中的 S(r),将 S(a)中的实参 a代替宏定义中的字符串
,PI*r*r”中的形参 r,得到 PI*a*a。这是容易理解而且不会发生什么问题的。但是,如果有以下语句:
area=S(a+B);这时把实参 a+B代替 PI*r*r中的形参 r,
成为 area=PI*a+B*a+B;请注意在 a+B外面没有括弧,
显然这与程序设计者的原意不符。原意希望得到
area=PI*(a+B)*(a+B);为了得到这个结果,应当在定义时,在字符串中的形式参数外面加一个括弧。即
#define S(r) PI*(r)*(r)在对 S(a+B)进行宏展开时,将
a+B代替 r,就成了 PI*(a+B)*(a+B)这就达到了目的。
(2) 在宏定义时,在宏名与带参数的括弧之间不应加空格,否则将空格以后的字符都作为替代字符串的一部分。例如,如果有
#define s (r)PI*r*r
被认为 s是符号常量 (不带参的宏名 ),它代表字符串,(r)
PI*r*r”。如果在语句中有 area=S (a);
则被展开为
area=(r) PI*r*r (a)
显然不对了。
有些读者容易把带参数的宏和函数混淆。的确,它们之间有一定类似之处,在引用函数时也是在函数名后的括弧内写实参,也要求实参与形参的数目相等。但是带参的宏定义与函数是不同的。主要有:
(1) 函数调用时,先求出实参表达式的值,然后代入形参。而使用带参的宏只是进行简单的字符替换。
例如上面的 S(a+b),在宏展开时并不求 a+B的值,
而只将实参字符,a+b”代替形参 r。
(2) 函数调用是在程序运行时处理的,分配临时的内存单元。而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。
(3) 对函数中的实参和形参都要定义类型,二者的类型要求一致,如不一致,应进行类型转换。而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时代入指定的字符即可。宏定义时,字符串可以是任何类型的数据。
例如:
#defineCHAR1CHINA(字符 )
#definea 3.6 (数值 )
CHAR1和 a不需要定义类型,它们不是变量,在程序中凡遇 CHAR1均以 CHINA代之 ;凡遇 a均以 3.6代之,显然不需定义类型。同样,对带参的宏:
#define S(r) PI*r*r
r也不是变量,如果在语句中有 S(3.6),则展开后为
PI*3.6*3.6,语句中并不出现 r。当然也不必定义 r
的类型。
(4) 调用函数只可得到一个返回值,而用宏可以设法得到几个结果。
例 9.4
#definePI3.1415926
#define CIRCLE(r,l,s,
v)L=2*PI*R;S=PI*R*R;V =4.0/3.0*PI*R*R*R
main()
{ float r,l,s,v;
scanf("%f",&r);
CIRCLE(r,l,S,v);
printf("r=%6.2f,l=%6.2f,S=%6.2f,
v=%6.2f\n",r,l,S,v);

经预编译宏展开后的程序如下:
main()
{ float r,l,S,v;
scanf(“%f”,&r); l=2*3.1415926
r;S=3.14159*r*r;v=4.0/3.0*3.1415926*r*r*r;
printf("r=%6.2f,l=%6.2f,S=%6.2f,
v=%6.2f\n",r,l,S,v);

运行情况如下:
3.5
r= 3.50,l= 21.99,S= 38.48,v= 179.59
请注意,实参 r的值已知,可以从宏带回 3个值 (l,S,
v)。其实,只不过是字符代替而已,将字符 r代替
R,l代替 L,S代替 S,v代替V,而并未在宏展开时求出 l,S,v的值。
(5) 使用宏次数多时,宏展开后源程序长,因为每展开一次都使程序增长,而函数调用不使源程序变长。
(6) 宏替换不占运行时间,只占编译时间。而函数调用则占运行时间 (分配单元、保留现场、值传递、
返回 )。
一般用宏来代表简短的表达式比较合适。有些问题,用宏和函数都可以。如:
#define MAX(x,y) (x)> (y)? (x)∶ (y)
main()
{ int a,b,c,d,t;

t=MAX(a+b,c+d);


赋值语句展开后为
t=(a+b)> (c+d)? (a+b)∶ (c+d);
注意,MAX不是函数,这里只有一个 main函数,
在 main函数中就能求出 t的值。
这个问题也可用函数来求:
int max(int x,int y)
{ return(x> y? x∶ y);}
main()
{ int a,B,C,D,t;

t=max(a+B,C+D);


max是函数,在 main函数中调用 max函数才能求出 t
的值。
请仔细分析以上两种方法。
如果善于利用宏定义,可以实现程序的简化,如事先将程序中的“输出格式”定义好,以减少在输出语句中每次都要写出具体的输出格式的麻烦。
例 9.5
#define PR printf
#define NL "\n"
#define D "%D"
#define D1 D NL
#define D2 D D NL
#define D3 D D D NL
#define D4 D D D D NL
#define S "%S"
main()
{ int a,B,C,D;
char string[]="CHINA";
a=1;B=2;C=3;D=4;
PR(D1,a);
PR(D2,a,B);
PR(D3,a,B,C);
PR(D4,a,B,C,D);
PR(S,string);

运行时输出以下结果:
1
12
123
1234
CHINA
程序中用 PR代表 printf。以 NL代表执行一次“换行”
操作。以 D代表输出一个整型数据的格式符。以
D1代表输出完 1个整数后换行,D2代表输出 2个整数后换行,D3代表输出 3个整数后换行,D4代表输出 4个整数后换行。以 S代表输出一个字符串的格式符。可以看到,程序中写输出语句就比较简单了,只要根据需要选择已定义的输出格式即可,
连 printf都可以简写为 PR。
可以参照例 9.5,写出各种输入输出的格式 (例如实型、
长整型、十六进制整数、八进制整数、字符型等 ),
把它们单独编成一个文件,它相当一个“格式库”,用 #inClude命令把它“包括”到自己所编的程序中,用户就可以根据情况各取所需了。
显然在写大程序时,这样做是很方便的。
9.2,文件包含”处理所谓“文件包含”处理是指一个源文件可以将另外一个源文件的全部内容包含进来,即将另外的文件包含到本文件之中。 C语言提供了 #inClude命令用来实现“文件包含”的操作。其一般形式为
#inClude,文件名”

#inClude <文件名 >
图 9.2
图 9.2表示“文件包含”的含意。图 9.2(a)为文件
file1.C,它有一个 #inClude <file2.C>命令,然后还有其他内容 (以 A表示 )。图 9.2(B)为另一文件 file2.C,
文件内容以 B表示。在编译预处理时,要对
#inClude命令进行“文件包含”处理:将 file2.C的全部内容复制插入到 #inClude <file2.C>命令处,即 file2.C被包含到
file1.C中,得到图 9.2(C)所示的结果。在编译中,
将“包含”以后的 file1.C(即图 9.2(C)所示 )作为一个源文件单位进行编译。
“文件包含”命令是很有用的,它可以节省程序设计人员的重复劳动。例如,某一单位的人员往往使用一组固定的符号常量 (如 G=9.81,
pi=3.1415926,e=2.718,C=……),可以把这些宏定义命令组成一个文件,然后各人都可以用
#inClude命令将这些符号常量包含到自己所写的源文件中。这样每个人就可以不必重复定义这些符号常量。相当于工业上的标准零件,拿来就用。
例 9.6可以将例 9.5程序改为:
(1) 文件 format.h
#define PR printf
#define NL "\n"
#define D "%D"
#define D1 D NL
#define D2 D D NL
#define D3 D D D NL
#define D4 D D D D NL
#define S "%S"
(2) 文件 file1.C
#inClude "format.h"
main()
{ int a,B,C,D;
char string[]="CHINA";
a=1;B=2;C=3;D=4;
PR(D1,a);
PR(D2,a,B);
PR(D3,a,B,C);
PR(D4,a,B,C,D);
PR(S,string);

注意,在编译时并不是作为两个文件进行连接的,
而是作为一个源程序编译,得到一个目标 (.oBj)文件。因此被包含的文件也应该是源文件而不应该是目标文件。
这种常用在文件头部的被包含的文件称为“标题文件”或“头部文件”,常以,h”为后缀 (h为
heaD(头 )的缩写 ),如,format.h”文件。当然不用
,.h”为后缀,而用,C”为后缀或者没有后缀也是可以的,但用,h”作后缀更能表示此文件的性质。
如果需要修改一些常数,不必修改每个程序,只需修改一个文件 (头部文件 )即可。但是应当注意,被包含文件修改后,凡包含此文件的所有文件都要全部重新编译。
头文件除了可以包括函数原型和宏定义外,也可以包括结构体类型定义 (见第 10章 )和全局变量定义等。
说明:
(1) 一个 inClude命令只能指定一个被包含文件,如果要包含 n个文件,要用 n个 inClude命令。
(2) 如果文件 1包含文件 2,而文件 2中要用到文件 3的内容,则可在文件 1中用两个 inClude命令分别包含文件 2和文件 3,而且文件 3应出现在文件 2之前,
即在 file1.C中定义:
#inClude "file3.h"
#inClude "file2.h"
这样,file1和 file2都可以用 file3的内容。在 file2中不必再用 #inClude <file3.h>了 (以上是假设 file2.h
在本程序中只被 file1.C包含,而不出现在其他场合 )。
(3) 在一个被包含文件中又可以包含另一个被包含文件,即文件包含是可以嵌套的。例如,上面的问题也可以这样处理,见图 9.3。它的作用与图 9.4所示相同。
图 9.3
图 9.4
(4) 在 #inClude命令中,文件名可以用双引号或尖括号括起来,如可以在 file1.C中用 #inClude <file2.h>
或 #inClude "file2.h"都是合法的。二者的区别是用尖括弧 (即 <file2.h>形式 )时,系统到存放 C库函数头文件所在的目录中寻找要包含的文件,这称为标准方式。用双引号 (即,file2.h”形式 )时,系统先在用户当前目录中寻找要包含的文件,若找不到,再按标准方式查找 (即再按尖括号的方式查找 )。一般说,
如果为调用库函数而用 #inClude命令来包含相关的头文件,则用尖括号,以节省查找时间。如果要包含的是用户自己编写的文件 (这种文件一般都在当前目录中 ),一般用双引号。
(5) 被包含文件 (file2.h)与其所在的文件 (即用
#inClude命令的源文件 file1.C),在预编译后已成为同一个文件 (而不是两个文件 )。因此,如果
file2.h中有全局静态变量,它也在 file1.C文件中有效,不必用 extern声明。
9.3 条件编译一般情况下,源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译,
也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。
条件编译命令有以下几种形式:
(1) #ifdef标识符程序段 1
#else
程序段 2
#endif
它的作用是当所指定的标识符已经被 #define命令定义过,则在程序编译阶段只编译程序段 1,否则编译程序段 2。其中 #else部分可以没有,即 #ifdef标识符程序段 1#endif这里的“程序段”可以是语句组,也可以是命令行。这种条件编译对于提高 C
源程序的通用性是很有好处的。如果一个 C源程序在不同计算机系统上运行,而不同的计算机又有一定的差异 (例如,有的机器以 16位 (2个字节 )来存放一个整数,而有的则以 32位存放一个整数 ),
这样往往需要对源程序作必要的修改,这就降低了程序的通用性。可以用以下的条件编译来处理:
#ifdef COMPUTER-A
#define INTEGER-SIZE 16
#else
#define INTEGER-SIZE 32
#endif
即如果 COMPUTER-A 在前面已被定义过,则编译下面的命令行:
#define INTEGER-SIZE 16
否则,编译下面的命令行:
#define INTEGER-SIZE 32
如果在这组条件编译命令之前曾出现以下命令行:
#define COMPUTER-A 0
或将 COMPUTER-A定义为任何字符串,甚至是
#define COMPUTER-A则预编译后程序中的
INTEGER-SIZE都用 16代替,否则都用 32代替。
这样,源程序可以不必作任何修改就可以用于不同类型的计算机系统。当然以上介绍的只是一种简单的情况,读者可以根据此思路设计出其他的条件编译。
例如,在调试程序时,常常希望输出一些所需的信息,而在调试完成后不再输出这些信息。可以在源程序中插入以下的条件编译段:
#ifdef DEBUG
printf("x=%D,y=%D,Z=%D\n",x,y,Z);
#endif
如果在它的前面有以下命令行:
#define DEBUG
则在程序运行时输出 x,y,Z的值,以便调试时分析。
调试完成后只需将这个 define命令行删去即可。有人可能觉得不用条件编译也可达此目的,即在调试时加一批 printf语句,调试后一一将 printf语句删去。的确,这是可以的。但是,当调试时加的 printf语句比较多时,修改的工作量是很大的。用条件编译,则不必一一删改 printf语句,只需删除前面的一条
,#define DEBUG”命令即可,这时所有的用 DEBUG
作标识符的条件编译段都使其中的 printf语句不起作用,即起统一控制的作用,如同一个“开关”一样。
(2) #ifndef标识符程序段 1
#else
程序段 2
#endif
只是第一行与第一种形式不同:将,ifdef”改为
,ifndef”。它的作用是若标识符未被定义过则编译程序段 1,否则编译程序段 2。这种形式与第一种形式的作用相反。
以上两种形式用法差不多,根据需要任选一种,视方便而定。例如,上面调试时输出信息的条件编译段也可以改为
#ifndef RUN
printf("x=%D,y=%D,Z=%D\n",x,y,Z);
#endif
如果在此之前未对 RUN定义,则输出 x,y,Z的值。
调试完成后,在运行之前,加以下命令行:
#define RUN
则不再输出 x,y,Z的值。
(3) #if表达式程序段 1
#else
程序段 2
#endif
它的作用是当指定的表达式值为真 (非零 )时就编译程序段 1,否则编译程序段 2。可以事先给定一定条件,使程序在不同的条件下执行不同的功能。
例 9.7输入一行字母字符,根据需要设置条件编译,
使之能将字母全改为大写输出,或全改为小写字母输出。
#define LETTER 1
main()
{ char str[20]="CLanGuaGe",C;
int i;
i=0
while((C=str[i])! =′\0′)
{ i++;
#if LETTER 条件编译
if(C> =′a′ && C< =′Z′)
C=C-32;
#else
if(C> =′A′ && C< =′Z′)
C=C+32;
#endif
printf("%C",C);


运行结果为:
CLANGUAGE
现在先定义 LETTER为 1,这样在对条件编译命令进行预处理时,由于 LETTER为真 (非零 ),则对第一个 if语句进行编译,运行时使小写字母变大写。
如果将程序第一行改为 #defineLETTER0则在预处理时,对第二个 if语句进行编译处理,使大写字母变成小写字母 (大写字母与相应的小写字母 ASCII
代码差 32)。此时运行情况为 ClanGuaGe
有的读者可能会问,不用条件编译命令而直接用 if
语句也能达到要求,用条件编译命令有什么好处呢?的确,此问题完全可以不用条件编译处理,
但那样做目标程序长 (因为所有语句都编译 ),运行时间长 (因为在程序运行时对 if语句进行测试 )。而采用条件编译,可以减少被编译的语句,从而减少目标程序的长度,减少运行时间。当条件编译段比较多时,目标程序长度可以大大减少。
本章介绍的预编译功能是 C语言特有的,有利于程序的可移植性,增加程序的灵活性。
习题
9.1 定义一个带参数的宏,使两个参数的值互换,并写出程序,输入两个数作为使用宏时的实参。输出已交换后的两个值。
9.2 输入两个整数,求它们相除的余数。用带参的宏来实现,编程序。
9.3 给年份 year,定义一个宏,以判别该年份是否闰年。
提示,宏名可定为 LEAP-YEAR,形参为 y,即定义宏的形式为
# define LEAP-YEAR(y) (读者设计的字符串 )
在程序中用以下语句输出结果,
if (LEAP-YEAR(year)) printf ("%D iS a leap
year",year);
else printf ("%D iS not a leap year",year);
9.4 请分析以下一组宏所定义的输出格式,
# define NL putchar (′\n′)
# define PR (format,value)printf
("value=%format\t",(value))
# define PRINT1(f,x1) PR(f,x1);NL
#define PRINT2 (f,x1,x2)PR (f,x1);PRINT1(f,x2)
如果在程序中有以下的宏引用,
PR (D,x);
PRINT 1(D,x);
PRINT 2(D,x1,x2);
写出宏展开后的情况,并写出应输出的结果,设
x=5,x1=3,x2=8。
9.5 设计所需的各种各样的输出格式 (包括整数、实数、字符串等 ),用一个文件名,format.h”,把这些信息都放到此文件内,另编一个程序文件,用 #inClude
“format.h”命令以确保能使用这些格式。
9.6 分别用函数和带参的宏,从 3个数中找出最大数。
9.7 试述“文件包含”和程序文件的连接 (link)的概念,二者有何不同?
9.8 用条件编译方法实现以下功能,
输入一行电报文字,可以任选两种输出,一为原文输出 ;
一为将字母变成其下一字母 (如‘ a?变成‘ B?……?Z?
变成‘ a?。其他字符不变 )。用 #define命令来控制是否要译成密码。例如,
# define CHANGE1则输出密码。若 #define
CHANGE0则不译成密码,按原码输出。