C 预处理器(preprocessor) 在源码编译之前对其进行一些文本性质的操作. 主要包括删除注释, 插入被 #include 指令包含的文件内容, 定义和替换由 #define 指令定义的符号以及确定代码的部分内容是否根据一些条件编译指令进行 编译.

预定义符号

由预处理器定义的符号.

符号 样例值 含义
_FILE_ name.c 进行编译的源文件名
_LINE_ 25 文件当前行的行号
_DATE_ "Jan 31 1997" 文件被编译的日期
_TIME_ "18:04:30" 文件被编译的时间
_STDC_ 1 如果编译器遵循ANSI C, 其值为1, 否则未定义

#define

#define name stuff

每当有符号 name 出现在这条指令后面, 预处理器就会把它替换成 stuff.

几个例子

#define reg register
#define do_forever for(;;)
#define CASE   break;case

如果定义中的 stuff 非常长, 可以分成几行, 除最后一行外, 每行的末尾都加上一个 反斜杠

#define DEBUG_PRINT  printf("File %s line %d:" \
                            " x=%d, y=%d, z=%d", \
                            __FILE__, __LINE__,\
                            x, y, z)

#define 机制允许把参数低缓到文本中, 这种实现通常称为宏(macro)或定义宏

#define name(parameter-list) stuff
其中 paramerter-list(参数列表)是一个由逗号分隔的符号列表, 它们可能出现在 stuff 中. 参数列表必须与 name 紧邻. 如有两者之间有任何空白, 参数列表会被 解释为 stuff 的一部分.

当宏被调用时, 名字后面是一个由逗号分割的值的列表, 每个值都与宏定义中的 一个参数相对应, 整个列表用一对括号包围. 当参数出现在程序中, 与每个参数对应的实际值都将被替换到 stuff 中.

#define SQUARE(x)  x * x

注意

int a = 5;
printf("%d\n", SQUARE(a + 1));
的结果将是11, 因为这个宏将按如下方式展开
printf("%d\n", a + 1 * a + 1);

#define SQUARE(x)  (x) + (x);

可以避免, 将被展开为

printf("%d\n", (a + 1) * (a + 1));

看看这个宏定义

#define DOUBLE(x) (x) + (x);

int a = 5;
printf("%d\n", 10 * DOUBLE(a));
将被展开为
printf("%d\n", 10 * (a) + (a));
在定义宏的时候, 在整个表达式两遍加上一对括号:
#define DOUBLE(x) ((x) + (x));

#define 替换

替换宏需要几个步骤:

  1. 首先对参数进行检查, 替换包含了由 #define 定义的字符
  2. 将替换文本插入到程序中原来文本的位置. 用值替换宏的参数名.
  3. 在此扫描结果文本, 如果还包含由 #define 定义的符号, 重复上述处理过程.

宏不可以出现递归

不会检查字符串常量.

如果想把宏参数插入到字符串常量, 有两种方法. C 语言中 临近的字符串自动会自动连接

#define PRINT(FORMAT,VALUE)        \
   printf("The value is " FORMAT "\n", VLAUE);
这种技巧只有当字符串常量作为宏参数给出时才能使用.

第二个技巧使用预处理器把宏参数转换为一个字符串. #argument 这种结构被预处理 翻译为 "argument".

#define PRINT(FORMAT,VALUE)         \
   printf("The value of " #VALUE    \
         " is " FORMAT "\n", VALUE);

int x = 22;
PRINT("%d", x + 3);
会产生下面输出:

The value of x + 3 is 25

## 结构把位于它两边的符号链接成一个符号. 允许宏定义从分离的文本片段创建 标识符.

#define ADD_TO_SUM(sumb_number, value) \
   sum ## sum_number += value

ADD_TO_SUM(2, 25);
最后一条语句把值 25 加到变量 sum5

宏与函数

由于调用函数返回的代码可能比实际执行小型计算的代码更大, 所以宏比使用函数在程 序规模和速度方面都更胜一筹.

更重要的是, 函数参数必须声明为一种特定的类型, 宏是与类型无关的.

使用宏的缺点就是, 每次使用宏时, 一份宏定义代码的拷贝都将插入到程序中.

带副作用的宏参数

命名约定

#undef

用于移除一个宏定义

命令行定义

Unix编译器中, -D 选项可以完成这项任务

-Dname
-Dname=stuff

条件编译

条件编译可以选择代码的一部分被正常编译还是完全忽略. 基本结构是 #if 指令和与其匹配的 #endif 指令.

#if constant-expression
   statements
#endif
还有可选的 #else 和 #elif 指令

是否被定义

#if defined(symbol)
#ifdef symbol

# if !defined(symbol)
#ifndef symbol

嵌套指令

文件包含

#include 指令使另一个文件的内容被编译. 一个头文件如果包含10个源文件中, 它实际上被编译了 10 次.

函数库文件包含

#include <filename>

本地文件包含

#include "filename"

嵌套文件包含

标准编译器必须支持至少 8 层的头文件嵌套.

可以像下面这样写, 来避免多重包含

#ifndef _HEADRNAME_H
#define _HEADERNAME_H
...
#endif

其他指令

#error 指令允许你生成错误信息:

#error text of error message

#line 用于通知预处理器 number 是一行输入的行号.

#line number "string"

如果给出了可选部分 "string", 预处理器就把它当做当前文件的名字.

值得注意的是, 这条指令将修改 __LINE__ 符号的值, 如果加上可选部分, 它还将 修改 __FILE__ 符号的值

#progma 指令是另一种机制, 用于支持因编译器而异的特性. 语法也是因编译器而异.

无效指令(null directive)就是一个#符号开头, 后面不跟任何内容的一行.