C 处理器可以是强大的工具,也可以成为不易发现的错误之源。
预处理器
- 用 gcc -E program.c 就能看到预处理器的输出
- 预处理器仅知道很少的 C 语言规则
- 所以很容易出现错误
- 对于复杂的程序,
- 检查预处理器的输出是找到这类错误的很好的办法
预处理器指令
大多数预处理指令属于下面 3 种类型之一:
- 宏定义。
#define
定义一个宏,#undef
删除一个宏定义。 - 文件包含。
#include
- 条件编译。
#if
,#ifdef
,#ifndef
,#elif
和#endif
指令可以让预处理器以测试的条件来确定是否将一段文本段包含到程序中。
剩下 #error
, #line
, #pragma
较少用到。
注意
- 指令都以
#
开头 指令总是在第一个换行符处结束,除非明确地指明要延续——在行末使用
\
。例如,下面的指令定义了一个宏来表示硬盘的容量(按字节算):1234TRACKS_PER_SIDE * \SECTORS_PER_TRACK * \BYTES_PER_SECTOR)指令可以出程序中的任何地方,甚至函数定义的中间。但是我们通常将
#define
和#include
指令放在文件的开始。- 注释可以与指令放在同一行。
宏定义
不需要使用等号
|
|
对 C 语言做小的修改
虽然这通常不是个好主意。比如:
|
对类型重命名
|
带参数的宏
|
|
如果后面的程序中有如下语句:
|
|
预处理器会将这些行替换为:
|
|
使用带参数的宏替代真正的函数有两个优点:
- 程序可能稍微快些。程序执行时调用函数通常会有些额外开销——存储上下文信息、复制参数的值等,而调用宏则没有这些运行开销。(C99 中的内联函数为我们提供了不使用宏而避免这一开销的办法。)
- 宏更“通用”。与函数的参数不同,宏的参数没有类型。只要预处理后的程序依然是合法的,宏可以接受任何类型的参数。如上例,可以用
MAX
宏来选出较大的一个。数的类型可以是int
,long
,float
,double
等。 - 编译后的代码通常会变大。宏的使用越频繁,效果越明显。
- 宏参数没有类型检查。预处理器不会检查宏参数的类型,也不回进行类型转换。
- 无法用指针指向一个宏。
宏可能会不止一次地计算它的参数。为了自我保护,最好避免使用带有副作用的参数。函数对它的参数只会计算一次,而宏可能会计算两次甚至更多次。如果参数有副作用:
12n = MAX(i++, j);// n = ((i++)>(j)?(i++):(j));
带参数的宏不仅适用于模拟函数调用,还经常用作需要重复书写的代码段模式。例如:
|
#
运算符
将宏的一个参数转换为字符串字面量。
|
|
##
运算符
使两个记号(如标识符)“粘合”在一起
|
|
一个更实用的例子:
|
|
如果需要一个针对 float
值的 max()
。下面是使用 GENERIC_MAX
宏来定义这一函数的方法:
|
|
宏的通用属性
替换列表可以包含对其他宏的调用
|
预处理器会不断重新检查替换列表,直到所有的宏名字都替换掉为止。
只替换完整的记号
预处理器会忽略前在标识符、字符常量、字符串字面量值之中的宏名。
宏不可以被定义两遍
除非新旧定义一样
用 #undef
取消定义
#undef N
会删除宏 N
当前的定义。(如果 N 没有被定义成一个宏,#undef
指令没有任何作用。)
宏定义中的括号
- 如果有运算符,始终将替换列表放在括号中:
#define TWO_PI (2*3.14159)
- 如果宏有参数,每个参数每次在替换列表中出现时都要放在圆括号中:
#define SCALE(x) ((x)*10)
如果没有括号,编译器可能会不按我们期望的方式应用运算符的优先级和结合性规则。
预定义宏
用 __DATE__
宏和 __TIME__
宏指明程序编译的时间。来帮助区分同一程序的不同版本。
|
|
可以使用 __LINE__
宏和 __FILE__
宏来找到错误。考虑被零除的定位问题。当 C 程序因为被零除而导致终止时,通常没有信息指明哪条除法导致错误。下面的宏可以帮助我们查明错误的根源:
|
|
__func__
标识符
每一个函数都可以访问 __func
标识符,它的行为很像一个存储当前正在执行的函数的名字的字符串变量。其作用相当于在函数体的一开始包含如下声明:
|
|
其中 function-name 是函数名。这个标识符的存在使得我们可以写出如下的调试宏:
|
|
对这些宏的调用可以放在函数体中,以跟踪函数的调用:
|
|
__func__
另一个用法:作为参数传递给函数,让函数知道调用它的函数的名字。
条件编译
#if
and #endif
|
|
defined
如果标识符是一个定义过的宏则返回 1,否则返回 0
|
|
和下面这段等价:
|
|
#elif
and #else
使用条件编译
条件编译用于调试是非常方便的,但是其应用不限于此。
在多台机器或多种操作系统之间可移植的程序
下面的例子中会根据 WIN32, MAC_OS 或 LINUX 是否被定义为宏,而将三组代码之一包含到程序中:
1234567
.........
可以用不同编译器编译的程序
为宏提供默认定义
|
条件屏蔽
|
|
其他指令
#error
指令
通常与条件编译指令一起监测正常编译过程中不应该出现的情况,往往预示着程序中出现了严重的错误。
|
如果试图在一台以 16 位存储整数的机器上编译这个程序,将产生一条出错消息:
Error directive: int type is too small
#error
指令通常会出现在 #if-#elif-#else
中的最后一部分:
123456789
.........
FAQ
究竟哪些常量需要定义成宏?
除了 0 和 1 以外的每一个数值常量都应该定义成宏。
使用宏来替换字符或字符串常量并不总能够提高程序的可读性,建议是:当常量被不止一次地使用,或以后可能需要修改常量时使用。
在执行预处理指令前,先处理注释。如果使用了条件屏蔽,而 #if
和 #endif
之间有未终止的注释,会引起错误。