C预处理器在源代码编译之前对其进行一些文本性质的操作。它的主要任务包括删除注释、插入被#include指令包含的文件的内容、定义和替换由#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令进行编译。
在#define中,如果定义的内容很长,可以分成几行,除了最后一行之外,每行的末尾都要加一个反斜杠\,例如:
#define DEBUG_PRINT printf("File %s line %d:" \
" x=%d, y=%d, z=%d", \
__FILE__, __LINE__, \
x, y, z)
一般不在#define中定义的语句中使用分号;,而是留到实际代码中使用。
#define机制包含了一个规定,允许把参数替换到文本中,这种实现通常称为宏。例如:
#define name(parameter-list) stuff
其中,parameter-list是一个由逗号分隔的符号列表,它们可能出现在stuff中。参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
当宏被调用时,名后面是一个由逗号分隔的值的列表,每个值都与宏定义中的一个参数相对应,整个列表用一对括号包围。当参数出现在程序中时,与每个参数对应的实际值都将被替换到stuff中。例如:
#define SQUARE(x) x * x
SQUARE(5); // 即 5*5
为宏定义采纳一种命名约定是很重要的,一个常见的约定就是把宏名字全部大写。
宏非常频繁地用于执行简单的计算,因为用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大,另外函数参数必须指定特定的类型,而宏可用于整型、浮点型以及其他任何可以正常使用的类型。
宏还可以用于一些函数无法实现的功能,例如:
#define MALLOC(n, type) \
((type *)malloc((n) * sizeof(type)))
有些宏还带有副作用,就是在表达式求值时出现的永久性效果。例如x++。
不要在一个宏定义的末尾加上分号,使其成为一条完整的语句。
不要忘记在宏定义中使用的参数的周围加上括号。
不要忘记在宏定义的两边加上括号。
#undef预处理指令用于移除一个宏定义:
#undef name
如果一个现存的名字需要被重新定义,那么它的旧定义首先必须用#undef移除。
在编译一个程序时,如果我们可以选择某条语句或某组语句进行翻译或者被忽略,常常会显得很方便。只用于调试程序的语句就是一个明显的例子。它们不应该出现在程序的产品版本中,但是你可能并不想把这些语句从源代码中物理删除,因为如果需要一些维护性修改时,你可能需要重新调试这个程序,还需要这些语句。
条件编译就是用于实现这个目的。使用条件编译,你可以选择代码的一部分是被正常编译还是完全忽略。用于支持条件编译的基本结构是#if指令和与其匹配的#endif指令。其形式为:
#if constant-expression
statement
#endif
或者
#if constant-expression
statement
#elif constant-expression
other statement
#else
other statement
#endif
所谓常量表达式,就是说它或者是字面值常量,或者是一个由#define定义的符号。如果变量在执行期之前无法获得它们的值,那么它们如果出现在常量表达式中就是非法的。例如:
#if DEBUG
printf("x=%d, y=%d\n", x, y);
#endif
如果想编译它时,只要使用#define DEBUG 1即可。
测试一个符号是否已经被定义也是可能的。在条件编译中完成这个任务往往更为方便,因为程序如果并不需要控制编译的符号所控制的特性,它就不需要被定义。这个测试可以通过以下任何一种方式进行:
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
每对定义的两条语句是等价的,但#if形式的功能更强,因为常量表达式可能包含额外的条件。
还可以嵌套使用#if条件编译,例如
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_of_option1();
#endif // OPTION1
#ifdef OPTION2
unix_version_of_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_of_option2();
#endif
#endif
为了帮助读者记住复杂的嵌套指令,为每个#endif加上一个注释标签是很有帮助的。
当头文件被包含时,位于头文件的所有内容都要被编译。这个事实意味着每个头文件只应该包含一组函数或数据的声明。和把一个程序需要的所有声明都放入一个巨大的头文件相比,使用几个头文件,每个头文件包含用于某个特定函数或模块的声明的做法更好一些。
函数库头文件包含使用以下语法
#include <filename>
本地文件包含使用以下语法
#include "filename"
嵌套#include 文件的一个不利之处在于它使得我们很难判断源文件之间的真正依赖关系。另一个不利之处在于一个头文件可能会被多次包含。
要解决这个问题,可以使用条件编译。如果所有的头文件都像下面这样编写:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H 1
// 想要在头文件内进行的操作
#endif
那么多重包含的危险就被消除了。
由于这种处理将拖慢编译速度(预处理器仍将读入整个头文件,但其内容被忽略),所以如果可能,应避免出现多重包含,不管是否由于嵌套#include文件导致。
#progma指令是一种机制,用于支持因编译器而异的特性。它的语法也是因编译器而异。从本质上说,#progma是不可移植的,预处理器将忽略它不认识的#progma指令,两个不同的编译器可能以两种不同的方式解释同一条#progma指令。
#error指令在编译时产生一条错误信息,信息中包含的是你所选择的文本。
#line指令允许你告诉编译器下一行输入的行号,如果它加上了可选内容,它还将告诉编译器输入源文件的名字。