4.11 C 预处理器
C 语言通过预处理器提供了一些语言功能
从概念上讲,预处理器是编译过程中单独执行的第一个步骤
两个最常用的预处理器指令是:
#include
指令(用于在编译期间把指定文件的内容包含进当前文件中)#define
指令(用任意字符序列替代一个标记)
本节还将介绍预处理器的其它一些特性,如条件编译与带参数的宏
4.11.1 文件包含
文件包含指令(即 #include
指令)使得处理大量的 #define
指令以及声明更加方便
在源文件中,任何形如:#include "文件名"
或 #include <文件名>
的行都将被替换为由文件名指定的文件的内容
如果文件名用引号引起来,则在源文件所在位置查找该文件
如果在该位置没有找到文件,或者如果文件名是用尖括号 <与> 括起来的,则将根据相应的规则查找该文件
这个规则同具体的实现有关
被包含的文件本身也可包含 #include
指令
源文件的开始处通常都会有多个 #include
指令
它们用以包含常见的 #define
语句和 extern
声明,或从头文件中访问库函数的函数原型声明,比如 <stdio.h>
严格地说,这些内容没有必要单独存放在文件中
访问头文件的细节同具体的实现有关
在大的程序中,#include
指令是将所有声明捆绑在一起的较好的方法
它保证所有的源文件都具有相同的定义与变量声明,这样可以避免出现一些不必要的错误
很自然,如果某个包含文件的内容发生了变化,那么所有依赖于该包含文件的源文件都必须重新编译
4.11.2 宏替换
宏定义的形式:#define 名字 替换文本
这是一种最简单的宏替换 —— 后续所有出现名字记号的地方都将被替换为替换文本
#define
指令中的名字与变量名的命名方式相同,替换文本可以是任意字符串
通常情况下,#define
指令占一行,替换文本是 #define
指令行尾部的所有剩余部分内容
但也可以把一个较长的宏定义分成若干行,这时需要在待续的行末尾加上一个反斜杠符 \
#define
指令定义的名字的作用域从其定义点开始,到被编译的源文件的末尾处结束
宏定义中也可以使用前面出现的宏定义
替换只对记号进行,对括在引号中的字符串不起作用
例如,如果 YES
是一个通过 #define
指令定义过的名字,则在 printf("YES")
或 YESMAN
中将不执行替换
替换文本可以是任意的,例如:#define forever for (;;) /* infinite loop */
该语句为无限循环定义了一个新名字 forever
宏定义也可以带参数,这样可以对不同的宏调用使用不同的替换文本
例如:#define max(A, B) ((A) > (B) ? (A) : (B))
使用宏 max
看起来很像是函数词用,但宏调用直接将替换文本插入到代码中
形式参数(在此为 A
、B
)的每次出现都将被替换成对应的实际参数
因此,语句 x = max(p + q, r + s);
将被替换为下列形式 x = ((p + q) > (r + s) ? (p + q) : (r + s));
如果对各种类型的参数的处理是一致的,则可以将同一个宏定义应用于任何数据类型
而无需针对不同的数据类型需要定义不同的 max
函数
仔细考虑一下 max
的展开式,就会发现它存在一些缺陷
其中,作为参数的表达式要重复计算两次,如果表达式存在副作用(比如含有自增运算符或输入输出),则会出现不正确的情况
例如:max(i++, j++) /* WRONG */
,将对每个参数执行两次自增操作
同时还必须注意,要适当使用圆括号以保证计算次序的正确性,考虑下列宏定义:
#define square(x) x * x /* WRONG */
当用 squrare(z + 1)
调用该宏定义时会出错:z + 1 * z + 1
但是,宏还是很有价值的
<stdio.h>
头文件中有一个很实用的例子:
getchar
与 putchar
函数在实际中常常被定义为宏,这样可以避免处理字符时调用函数所需的运行时开销
<ctype.h>
头文件中定义的函数也常常是通过宏实现的
可以通过 #undef
指令取消名字的宏定义,这样做可以保证后续的调用是函数调用,而不是宏调用:
#undef getchar
int getchar(void) { ... }
形式参数不能用带引号的字符串替换
但是,如果在替换文本中,参数名以 #
作为前缀则结果将被扩展为由实际参数替换该参数的带引号的字符串
例如,可以将它与字符串连接运算结合起来编写一个调试打印宏:
#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);
在实际参数中,每个双引号 "
将被替换为 \"
,反斜杠 \
将被替换为 \\
,因此替换后的字符串是合法的字符串常量
预处理器运算符 ##
为宏扩展提供了一种连接实际参数的手段
如果替换文本中的参数与 ##
相邻,则该参数将被实际参数替换
##
与前后的空白符将被删除,并对替换后的结果重新扫描
例如,下面定义的宏 paste
用于连接两个参数
#define paste(front, back) front ## back
因此,宏调用 paste(name, 1)
的结果将建立记号 name1
##
的嵌套使用规则比较难以掌握,详细细节请参阅附录 A
4.11.3 条件包含
还可以使用条件语句对预处理本身进行控制,这种条件语句的值是在预处理执行的过程中进行计算
这种方式为在编译过程中根据计算所得的条件值选择性地包含不同代码提供了一种手段
#if
语句对其中的常量整型表达式(其中不能包含 sizeof
、类型转换运算符或 enum
常量)进行求值
若该表达式的值不等于 0
,则包含其后的各行,直到遇到 #endif
、#elif
或 #else
语句为止
预处理器语句 #elif
类似于 else if
在 #if
语句中可以使用表达式 defined(名字)
该表达式的值遵循下列规则:当名字已经定义时,其值为 1
,否则,其值 为 0
例如,为了保证 hdr.h
文件的内容只被包含一次,可以将该文件的内容包含在下列形式的条件语句中:
#if !defined(HDR)
#define HDR
/* hdr.h 文件的内容放在这里 */
#endif
第一次包含头文件 hdr.h
时,将定义名字 HDR
此后再次包含该头文件时,会发现该名字已经定义,这样将直接跳转到 #endif
处
类似的方式也可以用来避免多次重复包含同一文件
如果多个头文件能够一致地使用这种方式
那么,每个头文件都可以将它所依赖的任何头文件包含进来,用户不必考虑和处理头文件之间的各种依赖关系
下面的这段预处理代码首先测试系统变量 SYSTEM
,然后根据该变量的值确定包含哪个版本的头文件:
#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
C 语言专门定义了两个预处理语句 #ifdef
与 #ifndef
它们用来测试某个名字是否已经定义
上面有关 #if
的第一个例子可以改写为下列形式:
#ifndef HDR
#define EDR
/* hdr.h 文件的内容放在这里 */
#endif