从头学C(42)C预处理器

0

从概念上讲,预处理器是编译过程中单独执行的第一个步骤。

最常用的两个预处理器指令我们前面已经见过很多次了,分别是:#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

Leave A Reply