C 语言的编译需要经过很多步骤,其中第一个步骤称为预处理阶段。这个阶段的主要任务包括删除注释、插入被#include指令包含的文件的内容、定义和替换#define指令定义的符号以及确定代码的部分内容是否应该跟绝一些条件编译指令进行编译。

#define

#define指令就是为数值命名一个符号。比如#define name stuff指令,有了它之后,每当有符号name出现在这条指令后面时,预处理器就会把它替换成stuff,比如下面几个例子:

// 为关键字 register 创建了一个简短的别名
#define reg             register
// 声明了一个更具描述性的符号用来替代实现无限循环的 for 语句
#define do_forever      for(;;)
// 定义了一个简短记法,在 switch 语句中使用,可以自动把一个 break 放在每个 case 之前
#define CASE            break;case

当然如果定义中的stuff非常长,那么也可以将它分成几行,除了最后一行之外,每行的末尾都需要加一个反斜杠。比如:

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

// 那么我们将可以很方便的插入一条调试语句打印
x *= 2;
y += x;
z = x * y;
log_debug;

很容易就发现上面的log_debug定义无法进行泛化,当然设计者也考虑到了这个问题,所以#define机制包括了一个规定,即允许把参数替换到文本中,这种实现一般称为,其声明方式如下:

define name(parameter-list) stuff

需要注意的是parameter-list是一个由逗号分隔的符号列表,他们可能出现在stuff中。参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。下面我们看一个具体的列子,以此了解宏定义的机制,并将它逐步优化改进:

#define SQUARE(x)   x * x

// 使用
SQUARE(5)
// 效果:5 * 5

考虑一下下面的代码段:

a = 5;
printf("%d\n", SQUARE(a + 1));

乍一看觉得这段代码将打印36这个值。但实际它却会打印11,我们仔细观察一下被替换的宏文本,即参数x被文本a + 1替换:

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

很容易想到对参数 x 加一个括号解决上述问题,即:

#define SQUARE(x)   (x) * (x)

// 上述打印将会被替换为
a = 5;
printf("%d\n", (a + 1) * (a + 1));

类似的我们可以再定义一个DOUBLE宏,即:

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

但是考虑下面的使用方式:

a = 5;
printf("%d\n", 10 * DOUBLE(5));

看上去它应该打印的结果是100,但事实上它打印的是55,我们再通过宏替换产生的文本观察问题:

printf("%d\n", 10 * (5) + (5));

所以我们需要在整个表达式两边加上一对括号。所有用于对数值表达式进行求值的宏定义都应该使用下面这种方式加上括号,避免在使用宏时,由于参数中的操作符或邻近的操作符之间不可预料的相互作用。

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

宏与函数

宏非常频繁的用于执行简单的计算,比如在两个表达式中寻找其中较大(小)的一个:

#define MAX(a, b)   ((a) > (b) ? (a) : (b))

那么为什么不使用函数来完成这个任务呢?首先用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大,所以使用宏比使用函数在程序的规模和速度方面都更胜一筹。

更为重要的是函数必须声明为一种特定的类型,所以它只能在类型合适的表达式上使用。但是上面的这个宏可以用于整型、长整型、单浮点型、双浮点型以及任何其它可以使用>操作符比较值大小的类型,即宏与类型无关

当然宏也有它的不利之处,因为每次在使用宏时,一份宏定义代码的拷贝都将插入到程序中,除非宏的定义非常短,否则使用宏将会大幅增加程序的长度。

也有一些任务根本无法使用函数实现,比如下面这个宏的第二个参数是一种类型,它无法作为函数参数进行传递。

#define MALLOC(n, type) ((type *)malloc((n) * sizeof(type)))

当宏参数在宏定义中出现的次数超过一次时,如果这个参数具有副作用,那么当使用这个宏的时候就可能出现危险,导致一些不可预料的后果。比如x++就是一个具有副作用的表达式,它会改变x的值,直接会导致下面的代码段出现不可预知的后果:

#define MAX(a, b)   ((a) > (b) > (a) : (b))

x = 5;
y = 8;
z = MAX(x++, y++);
// z = ((x++) > (y++) > (x++) : (y++))
属性 #define 宏 函数
代码长度 每次使用时,宏代码都被插入到程序中。除了非常小的宏志伟,程序的长度将大幅度增长 函数代码只出现于一个地方,每次使用这个函数时,都调用那个地方的同一份代码
执行速度 更快 存在函数调用/返回的额外开销
操作符优先级 宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能回产生不可预料的结果 函数参数只在函数调用时求值一次,它的结果传递给参数。表达式的求值结果更容易预测
参数求值 参数每次用于宏定义时,它们都将重新求值。由于多次求值,具有副作用的参数可能会产生不可预料的结果 参数在函数被调用前求值一次。在函数中多次使用参数并不会导致多种求值过程。参数的副作用不会造成任何特殊的问题
参数类型 宏与类型无关。只要对参数的操作是合法的,它可以使用于任何参数类型 函数的参数是与类型有关的。如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务时相同的

文件包含

我们知道#include指令可以使另一个文件的内容被编译,就像它实际出现于#include指令出现的位置一样。这种替换的执行方式很简单:预处理器删除这条指令,并用包含头文件的内容取而代之。这样一个头文件如果被包含到 10 个源文件中,它实际上被编译了 10 次。

基于这种替换的方式,当出现嵌套#include文件被多次包含时,就会出现问题:

#include "a.h"
#include "b.h"

// 如果 a.h 和 b.h 中都包含一个 #include x.h
// 那么 x.h 在此处就出现了两次

这种多重包含在绝大多数情况下出现于大型程序中,它往往需要很多头文件,所以要发现这种情况并不容易。但是我们可以使用条件编译来解决这个问题:

#ifndef _HEADER_NAME_H_
#define _HEADER_NAME_H_

/*
* All the stuff that you want in the header file
*/

#endif