预处理器
编译一个 C
程序涉及很多步骤。其中第 1
个步骤被称为预处理阶段。 C
预处理器在源代码编译之前对其进行一些文本性质的操作。它的主要任务包括删除注释、插入被 #include
指令包含的文件的内容、定义和替换由 #define
指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令进行编译。
预定义符号
测试代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
int main(int argc, char *argv[])
{
printf("hello tyustli\r\n");
printf("%s\r\n", __FILE__); /* 进行编译的源文件名 */
printf("%d\r\n", __LINE__); /* 文件当前行的行号 */
printf("%s\r\n", __DATE__); /* 文件被编译的日期 */
printf("%s\r\n", __TIME__); /* 文件被编译的时间 */
printf("%d\r\n", __STDC__); /* 如果编译器遵循 ANSI C ,其值就是 1,否则未定义 */
return 1;
}
/*
编译:gcc -o out test.c
运行:./out
结果:
hello tyustli
test.c
11
Dec 25 2020
21:40:13
1
*/
/*********************** end of file ****************************/
#define
定义
#define name stuff
有了这条指令以后,每当有符号 name
出现在这条指令后面时,预处理器就会把它替换成 stuff
。
如果 stuff
非常长,它可以分成几行,除了最后一行之外,没行的末尾都要加一个反斜杠,如下所示:
#define DEBUG_PRINT printf("File: %s line: %d:" \
"x = %d y = %d z = %d", \
__FILE__, __LINE__, \
x, y, z)
这条宏定义的语句 DEBUG_PRINT
后面没有分号,在调用这个宏的时候需要在语句末尾分号,如果你在宏的末尾加上分号,就会产生产生两条语句–一条 printf
语句后面再加一条空语句。有些场合只允许出现一条语句,如果放入两条语句就会出现问题,例如:
if(...)
DEBUG_PRINT
else
...
宏
#define name(parameter-list) stuff
parameter-list
是一个由逗号分隔的符号列表,它们可能出现在 stuff
中。参数列表的左括号必须与 name
紧邻。如果两者之间有任何空白存在,参数列表就会被解释为 stuff
的一部分。
注意
-
所有用于对数值表达式进行求值的宏定义都应该加上括号。避免在使用宏时,由于参数中的操作符或邻近的操作符之间不可预料的相互左右。
-
宏不可以出现递归
宏与函数
#define MAX(a, b) ((a) > (b) ? (a) : (b))
使用宏而不使用函数的优点
- 使用宏比使用函数在程序的规模和速度方面都更胜一筹
- 函数的参数必须声明为一种特定的类型,所以它只能在类型合适的表达式上使用。反之,上面这个宏可以用于整数、长整数、单浮点数、双浮点数以及其它任何可以用
>
操作符比较值大小的类型。换句话说,宏是与类型无关的。 - 还有一些任务根本无法用函数实现。例如下面的这个宏,这个宏的第
2
个参数是一种类型,它无法作为函数参数进行传递。#define MALLOC(n, type) \ ((type *)malloc((n) * sizeof(type)))
注意:这里的宏定义并没有用一个分号结尾,调用这个宏的时候应该以分号结尾。
缺点
- 和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝都将插入到程序中。除非宏非常短,否则使用宏可能会大幅度增加程序的长度。
带参数宏的副作用
当宏参数在宏定义中出现的次数超过一次时,如果这个参数具有副作用,那么当你使用这个宏时就可能出现危险,导致不可预料的结果
例如
#define MAX(a, b) ((a) > (b) ? (a) : (b))
x = 5;
y = 8;
z = MAX(x++, y++)
print(x, y, z)
宏展开就是
(x++) > (y++) ? (x++):(y++)
所以结果就是 x = 6 y = 10 z = 9
命名约定
一个常见的约定就是宏名字全部大写。
#undef
这条预处理指令用于移除一个宏定义。
#undef name
如果一个现存的名字需要被重新定义,那么它的旧定义首先必须用 #undef
移除掉
嵌套文件包含
#ifndef __HEADINCLUDE_H
#define __HEADINCLUDE_H
/*
* all the stuff that you want in the header file
*/
#endif /* __HEADINCLUDE_H */
#error
该定义在测试代码是否参与编译,或者某些宏必须定义时非常有用
#error text of err message
总结
- 不要在一个宏定义的末尾加上分号,使其成为一条完整的语句
- 在宏定义中使用参数,不要忘了在它们周围加上分号
- 不要忘了在整个宏定义的两边加上括号
- 避免使用
#define
指令定义可以用函数实现的很长序列的代码 - 避免使用
#define
宏创建一种新语言 - 采用命名约定,使程序员很容易看出某个标识符是否为
#define
宏