C预处理器在源代码编译之前对其进行一些文本性质的操作。它的主要任务包括删除注释、插入被#include指令包含的文件的内容、定义和替换由#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令进行编译。
1.预定义符号
预处理器定义的符号,它们的值或者是字符串常量,或者是十进制数字常量。__FILE__和__LINE__在确认调试输出的来源方面很有用处,__DATE__和__TIME__常常用于在被编译的程序中加入版本信息,__STDC用于哪些在ANSI和非ANSI环境都必须进行编译的程序中结合条件编译。
2.#define
1)文本替换的作用,#define name stuff
2)宏,允许把参数替换到文本中,#define SQUARE(x) (x) *(x)
3)#define替换,a)在调用宏时首先对参数进行检查,看看是否包含了任何由#define定义的符号,若是它们首先被替换;b)替换文本随后被插入到程序中原本文本的位置,对于宏,参数名被它们的值所替代;c)最后再次对结果文本进行扫描,看是否包含任何由#define定义的符号,若是就重复上述处理过程。
4)宏与函数,a)代码很可能比实际执行小型计算工作代码量少;b)宏定义与传达类型是无关的,函数则必须制定类型。
5)带副作用的宏参数,当宏参数在宏定义中出现的次数超过一次时,若这个参数具有副作用,可能导致不可预料的结果。副作用就是在表达式求值时出现永久性的效果。例如 x+1无副作用,x++就具有副作用。
6)命名约定,宏和函数的不同涉及代码长度、执行速度、操作符优先级、参数求值和参数类型等方面。
7)#undef,这条预处理指令用于移除一个宏定义。
8)命令行定义,许多C编译器提高一种能力,允许在命令行中定义符号,用于启动编译。当根据同一源文件编译一个程序的不同版本时,这个特性是很有用的。
3.条件编译
使用条件编译,可以选择代码的一部分是被正常编译还是完全忽略,用于支持的基本结构是#if指令和与其匹配的#endif指令。
#if constant-expression
statements
#elif constant-expression
other statements
#else
other statements
#endif
1)是否被重定义,测试一个符号是否被重定义是可能的,在条件编译中完成这个任务往往更方便,因此程序如果并不需要控制编译的符号所控制的特性,它就不需要被定义。
#if defined(symbol)
#ifdef symbol
#if !define(symbol)
#ifndef symbol
2)嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
uinx_version_of_option1();
#endif
#ifdef OPTION2
uinx_version_of_option2();
#endif
#elif #define(OS_MSDOS)
#ifdef OPTION2
msdos_version_of_option2();
#endif
#endif
4.文件包含
1)函数库包含,编译器支持两种不同类型的#include文件包含:函数库文件和本地文件。
2)本地文件包含,标准编译器自行决定是否把本地形式的#include和函数库形式的#include区别对待。
3)嵌套文件包含,在一个将被其它文件包含的文件中使用#include指令是可能的。嵌套#include文件的一个不利之处在于它使得我们很难判断源文件之间的真正依赖关系。
5.其它指令
#error指令允许生成错误信息。#progma指令是另一种机制,用于支持因编译器而异的特性。#line指令允许告诉编译器下一行输入的行号,如果它加上可选内容,它还将告诉编译器输入源文件的名字。
6.警告的总结
1)不要在一个宏定义末尾加上分号,使其成为一条完整的语句;
2)在宏定义中使用参数,但忘了在它们周围加上括号;
3)忘了在整个宏定义的两边加上括号;
7.编程提示总结
1)避免用#define指令定义可以用函数实现的很长序列的代码。
2)在那些对表达式求值宏中,每个宏参数出现的地方都应该加上括号,并且在整个宏定义的两边加上括号;
3)避免使用#define宏创建一种新语言;
4)采用命名约定,使程序员很容易看出某个标识符是否为#define宏;
5)只要合适就应该使用文件包含,不必担心它的额外开销;
6)头文件只应该包含一组函数和(或)数据的声明;
7)把不同集合的声明分离到不同的头文件中可以改善信息隐蔽;
8)嵌套的#include文件使我们很难判断源文件之间的依赖关系。