从概念上讲,预处理器是编译过程中单独执行的第一个步骤。
最常用的两个预处理器指令我们前面已经见过很多次了,分别是:#include 指令用于在编译期间把指定文件的内容包含进当前文件中;#define 指令用任意字符序列替代一个标记。
接下来,我们还会看到预处理器的一些其他特性。
第四章 函数与程序结构 >> 4.11 C预处理器
1. 文件包含
文件包含指令(即 #include)使得处理大量 #define 指令以及声明更加方便。
任何形如:
#include <文件名>
或者
#include "文件名"
的行都会被替换为“文件名”所指定的文件里的内容。
若文件名用双引号括起来,则在源文件所在位置查找该文件;若文件名用尖括号括起来,则根据相应的规则查找该文件,这个规则同具体的实现有关,大多情况下会去标准库中查找。
源文件的开始位置通常会包含多个 #include 指令,用来包含常见的 #define 语句和 extern 声明,或者从头文件中访问库函数的函数原型声明(比如<stdio.h>)
在大型程序中,#include 指令是将所有声明捆绑在一起的好办法,保证所有包含同一个文件的源文件都具有相同的定义和变量声明,可以避免一些不必要的错误。显然,如果被包含的文件里内容发生了变化,那么所有包含该文件的源文件都必须重新编译。
2. 宏替换
宏定义的形式:
#define 名字 替换文本
后续所有出现“名字”记号的地方,都将用“替换文本”代替(用双引号括起来的字符串不是记号,比如定义了一个名字为YES的宏,那么在printf(“YES”)中并不会被替换)。
其中“名字”的命名方式与普通变量相同,“替换文本”可以是任意字符串。
通常 #define 只占一行,“替换文本”就是该指令行尾部的所有剩余内容;但 #define 也可以占多行,不过需要在待续的行末尾加上反斜杠“\”。
#define 定义的名字,其作用域从其定义点开始,到被编译的源文件末尾结束。
替换文本可以是任意的,例如:
#define forever for(;;) //为无限循环定义了一个新名字:forever
宏定义也可以带参数,这样对不同的宏调用使用不同的替换文本。例如:
#define max(A, B) ((A) > (B) ? (A) : (B)) //定义了一个带参数的 max 宏
max 宏看起来很像函数调用,但它实际上是直接将替换文本插入到代码中,“形式参数” A 和 B 在每次宏调用的时候都会被替换成对应的“实际参数”。因此语句:
x = max(p+q, r+s);
会被替换为:
x = ((p+q) > (r+s) ? (p+q) : (r+s));
上述 max 宏还存在缺陷,因为作为参数的表达式会重复计算两遍,所以你可以预想得到,如果是 max(i++, j++) 调用会出现什么问题。
max 宏的替换文本中,A 和 B 的圆括号不能去掉,你可以试着把圆括号去掉之后,看看max(p+q, r+s) 替换后的文本会怎么样。所以必须要注意,适当使用圆括号,可以保证计算次序的正确性。
通过 #undef 指令取消名字的宏定义,这样做可以保证后续的调用是函数调用而不是宏调用:
#undef max int max(int x, int y) { ... }
形式参数不能用带引号的字符串替换。如果在替换文本中,参数名以 # 为前缀则结果将被扩展为由实际参数替换该参数的带引号字符串。比如写一个调试打印宏:
#define dprint(expr) printf(#expr " = %g\n", expr);
当调用 dprint(x/y) 时,该宏被扩展为:
printf("x/y" " = %g\n", x/y);
等价于
printf("x/y = %g\n", x/y);
在实际参数中,每个双引号 ” 都将被替换为 \”,反斜杠 \ 被替换为 \\,因此替换后的字符串是合法的字符串常量。
另外,预处理器运算符 ## 为宏扩展提供了一种连接实际参数的手段。如果替换文本中的参数与 ## 相邻,则该参数将被实际参数替换, ## 及其前后的空白符将被删除,对替换后的结果重新扫描。例如:
#define paste(front, back) front ## back
当宏调用 paste(name, 123)时,将建立记号 name123。
3. 条件包含
使用条件语句,可以实现对预处理本身进行控制。这种条件语句的值是在预处理执行的过程中进行计算的,是源代码在编译过程中根据条件值的不同,选择性地包含不同代码。
#if 语句对其后的常量整型表达式(不能包含 sizeof 、类型转换运算符、enum常量)进行求值,如果不为 0 (即为真),则包含其后的各行,直到遇到 #else、#elif 或 #endif 语句为止。
在 #if 语句中可以使用表达式 defined(名字),该表达式的规则是:如果“名字”已经定义了,其值为 1 ;否则为 0。
例如,在hdr.h中
#if !defined(HDR) #define HDR /* hdr.h 中的所有代码 */ #endif
这种方式可以保证 hdr.h 文件的内容只被包含一次。如果多个头文件能够一致地使用这种方式,那么,每个头文件都可以将它所依赖的任何头文件包含进来,用户不必考虑和处理头文件之间的各种依赖关系。
C语言专门定义了两个预处理语句 #ifdef 和 #ifndef,它们用来测试某个名字是否已经定义。所以上面的例子可以改写成:
#ifndef HDR #define HDR /* hdr.h 中的所有代码 */ #endif
最后,下面这个例子演示了:测试系统变量,再根据变量确定包含哪个头文件:
#if SYSTEM == SYSV #define HDR "sysv.h" #elif SYSTEM == BSD #define HDR "bsd.h" #elif SYSTEM == MSDOS #define HDR "msdos.h" #else #define HDR "default.h" #endif #include HDR