1
第 9章 预处理命令
2
9.0 编译预处理概述
C源程序编译之前,编译程序将对它作至少两个阶段的编译预处理 。
编译程序预处理阶段编译程序首先将源程序代码中的注解替换成空格字符;例如,
int /*变量 sum用于存放两个数的和 */ sum ;
将替换成:
int sum ;
然后,查找源代码中由续行符 (反斜线后紧跟换行符 )组成的物理行,并把它们合并成一个逻辑行;
例如,
3
printf (,That’s wond\
erful!” ) ;
将合并成:
printf (,That’swonderful!” ) ;
最后,对源代码中的那些仅用空白字符作分隔符的多个字符串直接量进行连接 。 例如,
printf (,A string is a sequence of characters”
“surrounded by”
“double quotes.” ) ;
将连接成:
printf (,A string is a sequence of characters
surrounded by double quotes.” ) ;
4
预处理程序预处理阶段
C源程序中像 #define,#include…,.等 这类特别代码:
称之为,预处理命令,,一共有三类,
(1) 宏定义命令
(2) 文件包含命令
(3) 条件编译命令它们由 C语言预处理程序 解释不是C语言本身的语言成分,它们有自己的独立语法,
5
一条预处理指令写在一行上 ( 逻辑行 )
预处理命令行可插入在源程序文件中的任何行中 。
必须以 #号开头,预处理命令的内容跟在 #号之后,
允许在 #号的前后插入若干 ( 至少一个 ) 空格或制表符 。 (注:允许仅由单一的 #号组成的空预处理命令行,空预处理指令行没有作用,处理时总被忽略 ) 。
预处理命令的作用域是从该行所在位臵起到它所在的源文件的末尾 。
新行 (回车换行符 ) 字符作为行结束符
6
在预处理程序预处理阶段,预处理程序扫描插入在源程序文件中的预处理命令,并实现预处理命令规定的功能 。 经预处理程序处理后的源程序文件,
再交给编译程序作编译处理 。
C预处理程序扩充了C语言的处理能力及程序设计环境,为有效地开发C语言程序提供了便利 。
本章将介绍各种预处理命令的功能与使用 。
7
9.1 宏定义所谓宏定义是指用一个标识符来代替 一个字符序列,因此也称之为标识符替换 。
宏定义用预处理命令 #define 进行 。
有两种宏定义形式:
一种为 类对象宏 ( object-like macro),这是无参数形式的宏定义;
另一种为 类函数宏 (function-like macro),它是带参数形式的宏定义 。
8
1,类对象宏定义类对象宏定义是一种最简单的宏定义,其一般形式为:
#define 宏名 宏主体其中:
宏名 ( macro name) 按 标识符规则确定。
习惯上宏名用大写字母表示但这不是规定,可用小写字母。
宏名前后至少有一个空格以便与前面的 define
及其后的 字符序列 之间隔开。
9
宏主体 也称之为替换列表,是语言符号组成的字符序列(注:可以为空,不是 C字符串)。
例如:
#define FOUR 2*2
类对象宏定义的作用是用 宏名 这个标识符来表示随后的 宏主体部分 给定的字符序列。
在预处理程序扫描源文件时,每遇到一个宏名,
便用 宏主体部分 所指定的字符序列替换(替换过程称为,宏展开,)。 注意,包含在 字符串常量中 的宏名和位于 注解行中 的宏名不被替换! 例如,
10
#define YES 1.0
#define TABSIZE 100
main ( )
{
float y ;
int table [ TABSIZE ] ;
y = YES ;
printf ( "YES = %f ",y ) ;
}
table[TABSIZE];
将 被 替 换 成,
table [100] ;y = YES ;将被替换成,
y = 1.0 ;
字符串中的 YES将不被替换
11
替换后的结果将交给C编译程序进行编译,至于宏展开后的结果形式是否符合C语言语法规则,将由
C编译程序检查语法上的正确性 。
例如将上例子写成为:
#define TABSIZE 100; /*以分号 ;结束 */
则宏展开后变成为:
int table [100;] ;
C编译程序将指出其错误,但预处理程序不管。
12
宏展开工作是在内存中进行的,因此并不改变源文件本身,程序设计者也不能直接见到宏展开后的结果形式。
某些C编译系统提供专用工具,用以查看宏展开后的结果形式。若上例存放在 file.c中,则可在命令行上打入如下命令:
cpp -P- file.c /* P为大写 */
将产生的宏展开后的结果文件 file.I 。 相关内容请查阅所用 C编译系统的用户手册 。
13
若宏主体比较长,在一行上写不下,换行书写不能简单地按回车换行书写 ( 因预处理命令行总是以新行字符结束)。
正确的续行书写方法是在适当的位臵断开,并在断开处插入一个反斜杠,\” 字符后再按回车,而把断开点后的那部分内容写入到下一行中去 。
例如,
#define mac,aaaaaaaaaaabbbbbbb\
ccccc”
main ( )
{
char str[30]=mac ;
printf(,%s\n,,str ) ;
}
14
#define UP 2
#define DN ( 2 + UP )
#define HI DN / 2
main ( )
{
float area ;
area = ( ( UP + DN ) * HI ) / 2 ;

}
预处理程序允许在一个类对象宏定义命令行中使用先前已定义过的宏名 。 例如:
area = ( ( 2 + ( 2 + 2 ) ) * ( 2 + 2 ) / 2 ) / 2 ;
15
宏名的有效范围是从它的定义点开始到它所在源文件的未尾 。 但可用 #undef预处理命令来改变一个宏名的作用域,使宏名局部化 。
#undef 预处理命令的作用是用来取消前面定义过的某个宏名的定义 。 它的一般形式为:
#undef 宏名其中,宏名,为前面已定义过的而在这里要被取消其定义的宏名 。
例如:
16
#define SIZE 100
main ( )
{
int table [ SIZE ] ;
#undef SIZE /* 从该点起,宏名 SIZE不再有效 */

}
17
main ( )
{
int x,y,z ;
for ( x=0 ; x<2 ; x++ ) {
#define XYZ 100
y = XYZ ;
#define XYZ 1000
z = XYZ ;
printf ( "%d %d\n",y,z ) ;
}
}
例如:
一个源程序文件中可以含有若干个 #define命令,
不同的 #define 命令中指定的,宏名,都不能相同。
但在 TC2.0 中是允许的,它将自动取代前一次的该
,宏名,的定义。
/* y = 100 ; */
/* z = 1000 ; */
18
宏名必须在源文件中使用该宏名的位臵之前定义,否则将不被替换。
float PI = 3.14 ;
#define PI 3.1 /*PI的作用域从该点开始 */
main ( )
{
printf (,%f \n”,PI ) ;
}
这个 PI将不被替换 !
19
2,类函数宏定义类函数宏是指其定义形式和作用都与函数相似的宏定义 (所以称之为类函数宏 )。简单地说,类函数宏即是在宏名后可指定若干参数的宏定义形式:
#define 宏名 ( 宏参数 1,宏参数 2,..,) 宏替换主体这儿 不能有空格这儿 至少 要有一个空格宏参数 i 是标识符,是宏名所带的参数。
20
类函数宏在程序中的引用形式为:
宏名 ( 替换参数 1,替换参数 2,,...替换参数 n )
,替换参数 i”对应于宏定义中的,宏参数 i”,但它们之间只有参数个数,顺序的对应,而 不存在类型一致的对应问题 。
对于如下宏定义:
#define MAX(A,B) ( (A) >(B)? (A),(B) )
若在源文件中含有如下宏调用:
x = MAX ( p+q,r+s ) ;
x=( ( p+q ) > ( r+s )? ( p+q ),( r+s ) ) ;
替换成
21
关于类函数宏的定义及其调用要注意如下几个方面:
所有的宏参数都应该在其后的,宏替换主体” 中出现,但不出现也是允许的,对宏展开无影响。
例如:
#define PRINT( V,F ) printf ( "%f\n",V )
的宏调用,
PRINT ( a,b ) ;
将被替换成,
printf ( "%f\n",a ) ;
22
对 出现在宏定义的,宏替换主体,中 宏名,或它的 宏调用形式,宏展开时对它们并不作任何替换 。
但是,在它之前定义的宏名及其宏调用形式出现在
,宏替换主体,中的情况则是正确的宏调用,宏展开时要对它们实施替换 。
#define AbC(V) sqrt(V)
#define ABC(D) sin(1)*ABC(2)+ABC/AbC(3)
main ( )
{
float a = ABC ( 4 ) ;
……
}
例如:
不被替换!
这将被替换
sin(1)*ABC(2) + ABC/sqrt (3) ;
替换成
23
与类对象宏定义一样,包含在一个字符串直接常量及注解中的宏调用形式,不进行替换。
例如,对以下的宏定义:
#define PRINT( V,F ) printf ( "V=%F \n",V )
的宏调用,
PRINT ( a,b ) ;
将被替换成,
printf ( "V=%F \n",a ) ;
24
#define NO(A) aAa
#define YES( A,B ) 12AB
main ( )
{
int a,NO(a)=888 ;
a = YES ( 3,4 ) ;
printf ( "%d,%d \n",a,NO(a) ) ;
}
由于对常量中的宏调用不进行替换,所以不能企图通过宏替换的方法去形成一个常数或一个标识符。
例如:
不会得到,
1234
将替换成:
12AB
不会得到,
aaa
将替换成:
aAa
不会得到
aaa
将替换成:
aAa
25
类函数宏调用中的替换参数可以是该宏定义的另一个宏调用。 例如:
#define VER(A) A
main ( )
{
int b = 1111,a = 2222 ;
printf ( "%d\n",VER( VER(a) ) ) ;
}
该程序运行的输出是 2222 。其替换顺序是:
VER( VER(a) )
└──┘

└───┘

26
类函数宏在很多方面都与函数类似,尽管许多计算任务既可利用类函数宏来完成,也可以用函数实现,但概念完全不同,不能将它们混淆 。 下面列出它们的一些主要区别:
① 函数的形参与实参都有数据类型,且它们必须一一对应;而类函数宏与替换参数仅有参数个数,
顺序的一致,不存在类型一致的问题;
② 函数调用要引起执行控制的转移,而宏调用只引起简单的替换操作,且它们被处理的时刻也不同,函数调用开销要比宏调用多;
27
③ 函数调用时实参先传递给形参,再求出实参的值,而宏调用仅替换,并不求值;
④ 函数调用不改变代码形式,而宏调用要改变代的形式( 但通常宏调用会增加程序的规模) ;
⑤ 函数调用不能在赋值号的左边出现,而宏调用既可出现在赋值号的左边,也可出现在赋值号的右边。
28
为避免不必要 的错误,在某些情况下需要对
“宏替换主体” 适当使用括号。
的替换结果将是,z+1*z+1,其效果是计算 2z+1,
而不是原意计算 (z+1)2 。
square ( z+1 )
的宏调用
#define square(x) x*x
如对于如下宏定义:
29
某些简单的函数定义可以使用类函数宏来定义例如:
#define MAX( A,B ) ( (A)>(B)? (A),(B) )
相当于定义了一个求两者中大者的函数。
getchar 和 putchar 就是用类函数宏定义来实现的,它们的定义如下:
#define getchar( ) getc(stdin)
#define putchar(c) putc( c,stdout )
注意,没有宏参数是允许的 。
30
#define six ( A,B,C ) A/a+2*3-B C
main ( )
{
float a = six ( 1,,4 ) ;
……,
}
宏调用的替换参数表中允许使用空格作为宏替换参数。
main ( )
{
float a = 1/a+2*3- 4 ;
……
}
宏展开的结果为:
例如:
空格作宏替换参数
31
3,预处理运算符字符串化运算符,#
#运算符的作用是把宏参数转化为相应的字符串 。 如果在宏替换主体中的某个宏参数前冠以 #号,
那么宏展开时将把替换参数的两边再上双引号后
( 变成字符串 ) 再进行替换 。
例如:
#define PR(V) printf ( #V "= %d\n",V )
其宏调用,PR(a); 宏展开后的形式是:
printf ( "a" "= %d\n",a ) ;
即,printf (,a=%d \n”,a ) ;
32
组合运算符,##
##运算符的功能是将两个替换参数组合成单个参数 。
如对于下面的类函数宏定义:
#define VAR ( i,j ) i##j
若有宏调用:
VAR ( x,6 )
那么宏展开时将把 i##j 替换成 x6 。
33
要定义参数个数不定的类函数宏,只要在宏参数表的最后放臵一个省略号 … 参数,然后在宏替换主体的最后放臵一个预定义宏 __VA_ARGS__ 。
类函数宏 PR可以接收若干个参数,如对于如下程序段中的两次 PR宏调用:
4,宏参数个数不定的类函数宏
#define PR ( … ) printf ( __VA_ARGS__)
__VA_ARGS__,记忆” 给出的替换参数在宏替换主体中的替换位臵及本次接收的参数个数。
例如:
34

PR (,Hello world!” ) ;

PR(“%disn’t smaller then %d.\n”,begin,end) ;


printf (,Hello world!” ) ;

printf (,%d isn’t smaller then %d,\n”,begin,end ) ;

第一次宏调用给出了一个替换参数,第二次宏调用给出了三个替换参数,而宏展开后的代码为:
35
#include <stdio.h>
#include <math.h>
#define PR(X,...) printf(“Message” #X,:,__VA_ARGS__)
main ( void )
{
double x = 48,y ;
y = sqrt (x) ;
PR ( 1,,x = %g\n”,x ) ;
PR ( 2,“x = %.2f,y = %.4f\n”,x,y) ;
}
下面是一个使用了 # 运算符及宏参数个数不定的类函数宏的示例:
36
类函数宏 PR是至少有一个参数的宏定义,因为它指定了第一个宏参数 X。
例中的第一次宏调用给出了 3个替换参数,第一个 参 数 1 替 换 宏 参 数 X,其 余 两 个 用 于 替 换
__VA_ARGS__,展开后成为:
printf (,Message”“1”“:”,x = %g\n”,x ) ;
对前面四个 C字符串连接后,最终的展开结果是:
printf (,Message 1,x = %g\n”,x ) ;
至此,不难理解例中的第二次宏调用的展开 。
注意,在进行不定参数替换时,会自动在替换的参数之间添加逗号分隔符 。
37
5,预定义宏在程序设计中,程序设计者可以直接使用由 C
标准定义的如下几个预定义宏:
——LINE— — ——FILE— —
——DATE— — ——TIME— —
这些宏的作用与意义是:
— —LINE— —给出当前其所在源文件中的行号,
行号以十进制整型常量表示,从 1开始编号 。
— —FILE— — 给出当前正在处理的源文件的名字,它是一个字符串 。
38
— —DATE— — 和 — —TIME— — 分别指出预处理程序处理当前源文件的日期与时间,它们都是字符串 。
日期和时间格式可能随C版本的不同而不同 。
日期和时间值指开始处理该源程序时的日期和时间,它们在整个处理过程中其值不变 。
不同C语言版本可能还支持其它的预定义宏,
使用时读者应查阅相关手册 。
下面的示例说明了某些预定义宏的使用效果,
假定源程序文件存放在 noname.c中 。
39
main ( )
{
int a = ——LINE—— ;
printf (,\nThis is line %d\n,,a ) ;
printf (,This is line %d \n,,——LINE— —) ;
printf (,The file is %s \n,,——FILE— — ) ;
printf (,The date and time is %s\t%s\n,,
——DATE— —,——TIME—— ) ;}
This is line 3
This is line 5
The file is NONAME.C
The date and time is May 9 2008 19:30:39
程序输出,
40
9.2,文件包含,处理在前面章节的例子中多次使用了形如,
#include,filename,
或 #include <filename>
它们被称为,文件包含,预处理命令行 。
其中,
include 是文件包含预处理命令
filename 是被包含的文件名 。
一个源程序文件中可以插入若干条这样的文件包含预处理命令行 。
编译系统提供的所有标准头文件都以,.h”
扩展名
41
C标准定义了若干标准头文件,它们大多数都与函数相关,其内容是相关函数的函数原型说明,及编译程序产生目标代码所需的信息 。
常用的标头文件
42
在预处理程序文件时,每遇到包含文件预处理命令行,都用 filename文件中的全部内容 替代这一行,使其成为源程序文件的一部分参与编译 。 因此被包含文件的内容一定为C源代码形式 。
#include 命令行不必须放臵在源程序文件的首部,被包含的文件也不限制为编译系统提供的标准头文件;而且包含文件的后缀也不必为,.h”。
程序设计者可根据需要自行确定包含文件的扩展名、名字、内容、及 #include 命令行在源程序文件中的位臵。
43
被包含的文件名可括在双引号或尖扩号中,这两种形式的 差别在于预处理程序查找要被包含的文件的路径不同。
若包含的文件名被括在双引号中,且是不含路径的文件名 例如,
#include,file.c”
则首先从包含该命令行的源程序文件所在的目录下去查找这个包含文件,如果下找不到,则再从C编译系统指定的目录 (include目录 )中查找。
44
如果括在双引号中的包含文件是带有路经的文件名,预处理程序将直接按指定的路径去查找被包含的文件 。
#include,c:\cpp\include\myfile.h”
例如:
这将直接从 c 盘的 cpp子目录下的 include子目录中去查找包含文件 myfile.h。
45
若括在尖括号中仅是不含路径的包含文件名,则仅从指定的标准目录 include下去查找;
例如:
#include <stdio.h>
#include <file.c>
若括在尖括号中的是含有路径的文件名,则按指定的路径去寻找被包含文件。
例如:
#include <c:\test\file.c>
从 C盘的 test子目录中去寻找包含文件 file.c。
46
#include,f2.c”
源程序文件 f1.c
#include,f3.c”
源程序文件 f2.c 源程序文件 f3.c
…… …… ……
#include命令可以嵌套使用,亦即 #include包含的文件中又有另一个 #include命令行,C标准规定最少应能处理 15层嵌套包含 。 例如:
47
经预处理程序处理后的结果是:
f3.c
f2.c
f1.c
48
下面的包含文件处理方式与上述嵌套包含文件的效果是相同的:
#include,f3.c”
源程序文件 f1.c
#include,f2.c”
源程序文件 f2.c 源程序文件 f3.c
…… …… ……
49
文件包含特性也有它的不足之处 。 因为被包含的文件都必须是源代码文件,所以当某一个包含文件被修改后 ( 哪怕是一个小小的修改 ),
那么所有依赖于它 ( 或与它相关 ) 的全部文件都必须重新编译 !
在程序设计中,利用文件包含特性可以很方便地使用现有的程序模块,避免程序模块的重复编写。
50
9.3 条件编译对于一个大型复杂的程序,往往根据需要有选择地编译源代码中的一部分而不是全部。
C编译系统的预处理程序支持这种功能,并把它称之为条件编译。即用预处理命令告诉编译程序,根据给定的条件确定编译源代码的范围。
例如:某客户仅定购了某软件的一个子集。 软件商一般不会无偿提供全部软件,而要对软件功能进行适当的裁减,形成一个客户所需特殊版本。这就需要编译部分代码的功能。
51
52
这六条预处理命令可以用来组成如下的几种常用的基本控制结构形式:
53
54
对于结构 (1),(2),(3),若给定的条件为真,
则对其后的,源代码块,进行编译,否则,编译时将忽略,源代码块,( 其实,预处理程序实际处理时把其后源的代码块用相应多的空行替代之 ),
而直接去编译 #endif后的源代码部分 。 例如:
#if SYSTEM ==1
#include,ibm.h”
#endif
55
#define YXF header
#ifdef YXF
printf (,Hi YXF\n” ) ;
#else
printf (,Hi anyone\n” ) ;
#endif`
对于结构 (4),(5),(6),若给定的条件为真,
则编译,源代码块 1,部分,否则将忽略,源代码块 1” 部分,而去编译 #else后的,源代码块 2” 部分。
例如:
56
对于结构 (7),这是一种多路判定结构,从条件 1开始,一旦测试到某个条件 i为真,则源代码块 i
被编译,而所有其他的源代码块将被忽略;如果所有的条件 ( 从 1 到 n-1) 都不成立,且包含 #else
命令部分,则源代码块 n被编译,而所有其他的源代码块将被忽略;因为在结构 (7) 中允许没有 #else命令部分,在这样的情况下,如果所有的条件 ( 从 1
到 n-1) 都不成立,则整个结构 (7)包含的全部源代码将被忽略 。
例如:
57
#if SYSTEM ==1
#include,IBM.h”
#elif SYSTEM ==2
#include,Honeywell.h”
#elif SYSTEM ==2
#include,Microsoft.h”
#else
#include,other.h”
#endif
58
上面所有的结构形式都可以互相嵌套使用 。 对于互相嵌套的情况,总是把首先遇到的 #endif或
#else与最近的 #if,#ifdef,#ifndef相匹配 。 C99允许嵌套至少 63层 。
#ifdef 和 #ifndef 指令可以分别写成,
#if defined (宏名 )
与 #if !defined (宏名 )
这里 defined 是一个预处理运算符,它的操作对象如果已用 #define 定义过,那么 defined返回 1 ;否则返回 0 。这种写法的好处在于,一方面它可以与
#else,#elif 指令一起使用,构成多路判定结构;另一方面,因 #if 后面的条件是一个表达式,便于给出较复杂的条件。
59
例如:
#if defined(——TINY——)||defined(——SMALL——)||
defined(——MEDIUM——)
#define NULL 0
#else
#define NULL 0L
#endif
60
条件编译的 #if控制结构形式类似于C的 if控制结构形式 。 它们的主要差异是:
(1)预处理程序不是用大括号 ( {…… }) 来括住代码块,而用 #else( 如果需要 ) 和 #endif(必须使用 )来括住代码块 ;
(2)C的 if控制结构包含的所有代码都被编译 ( 要生成目标代码 ),而条件编译的 #if控制结构包含的代码仅当指定的条件为真的代码块才被编译 。
61
#include <stdio.h>
#define JUST_CHECKING
define LIMIT 4
int main(void)
{
int i,total = 0 ;
for ( i = 1 ; i <= LIMIT ; i++ ) {
total += 2*i*i + 1 ;
#ifdef JUST_CHECKING
printf (,i=%d,running total = %d\n”,i,total ) ;
#endif
}
printf (,Grand total = %d\n”,total ) ;
return 0 ;
}
条件编译命的使用是容易的,下面是一个简单的例子。