第十四章 预处理器
预处理器的工作方式
预处理器的行为是由指令控制的。这些指令是由#字符开头的一些命令。比如 #define 和 #include 。
#define
定义了一个宏——用来代表其他东西的一个名字。当宏在后面的程序中用到时,预处理器扩展它,将宏替换为它所定义的值。
#include
指令告诉预处理器打开一个特定的文件,将它的内容作为正在编译的文件的一部分。
my note
可以使用gcc -E src.c
指令来查看预编译结果。
预处理指令
常见预处理指令包括:
-
宏定义。
#define
定义一个宏,#undef
删除一个宏。 -
文件包含。即
#include
-
条件编译。
#if #ifdef #ifndef #elif #else #endif
指令的通用规则有:
-
都以#开始。
-
在指令的符号之间可以插入任意数量的空格或横向制表符。
-
指令总是在第一个换行符处结束,除非明确地指明要继续,用\字符换行。
-
指令可以出现在程序的任何地方。
-
注释可以与指令放在同一行。
宏定义
宏定义的作用范围从定义处开始到本文件末尾。
简单宏定义
简单宏定义的格式如:
#define <宏名> [替换列表]
替换列表中可以有空格。甚至可以没有替换列表,此时宏替换后,就等于删除了这个宏一样。
简单的宏定义一般用于:
- 给字面量取一个别名
- 辅助条件编译
带参数的宏定义
格式如:
#define <宏名>(x1, x2, ..., xn) [替换列表]
注意点:
-
宏名和参数列表的括号之间不能有空格,不然就是一个简单宏了
-
参数列表可以为空,这样的宏使用起来就像一个函数
-
参数只会替换记号,字符串内的同名单词并不会被替换
带参数的宏一般用于:
- 替代一些小的函数,这样程序的执行效率会高一些,并且函数可能更加通用,因为宏不必检查参数类型
#号和##号
宏替换列表中有两个特殊符号:#和##,它们有如下的意义:
- #号代表参数会被替换成一个字符串字面量,例如 :
#define PRINT_INT(n) printf(#n " = %d\n", n)
#n会被替换成"n",相邻的字符串字面量可以连起来形成一个字符串字面量,所以PRINT_INT(a)的宏替换结果是:
printf("a = %d\n", a);
- ##代表将两边的记号连接在一起,成为一个记号,一个典型的例子:
#define GENERIC_MAX(type) \
type type##_max(type x, type y) \
{ \
return x > y ? x : y; \
};
这个宏定义定义了一个取最大值的函数,可以方便的为这个函数指定比较类型。
值得注意的是,#和##都在简单的宏替换后起作用。
宏定义中的圆括号
如果宏定义的替换列表是一个表达式,那么为其增加圆括号是必不可少的工作。
这是因为如果不加圆括号,在宏替换后,新的表达式可能会破坏替换列表表达式的运算优先级。
在替换列表表达式中使用圆括号有两条规则:
- 用圆括号将替换列表括起来
- 用圆括号把每个宏参数括起来
一个安全的宏的例子:
#define SUM(x, y) ((x) + (y))
创建较长的宏
一些废话:
宏函数展开后,实际上只有一行。而编写的时候为了好看,可以用\
作为换行连接符号。
另外,宏函数使用时看上去应该像普通函数一样:后面也要加分号。所以宏函数的替换列表的末尾应该没有分号。
直接上书上所给的解决方案:
#define ECHO(str) \
do \
{ \
gets(str); \
puts(str); \
} while(0)
// use
ECHO(str);
预定义宏
常用预定义宏:
宏 | 说明 |
---|---|
__LINE__ |
行号,十进制常数 |
__FILE__ |
文件名 |
__DATE__ |
文件编译时的日期 |
__TIME__ |
文件编译时的时间 |
文件名,日期,时间的预定义宏展开后都是一个字符串变量。行号是一个整型变量。
另外,不同的系统会定义不同的预定义宏,来标识其编译平台。如:
-
Linux下,
__unix
-
Windows下,
_WIN32
这种预定义宏配合条件编译就可以做到跨平台编译代码。
特殊的预定义宏__VA_ARGS__
C99标准中,有一个特殊的预定义宏,它的作用是替换可变参数列表(...),但它要和##符号配合使用,此时##的意义不再是连接,而是:当可变参数列表为空的时候,去除__VA_ARGS__
前面的逗号,从而避免编译错误。
一个典型的例子:
#define CONSOLE_DEBUG(fmt, ...)\
printf("FILE: "__FILE__", LINE: %05d "fmt"\n", __LINE__, ##__VA_ARGS__);
__FUNCTION__
这个宏代表了当前执行函数的函数名字符串。
条件编译
条件编译指令排除了不应该出现的文本。只有通过了条件编译的文本块才会被交给编译器编译。
条件一般是一个普通的宏。
书写格式如:
#if MACRO
code
#elif MACRO
code
#else
code
#endif
defined 运算符
defined 运算符仅用于预处理器。
#if defined(DEBUG)
...
#endif
如果标识符 DEBUG 是一个定义过的宏,则返回1,否则返回0。 defined 返回1意味着通过条件。
指令说明:
-
#if, #elif可以判断这个宏的值,如果是0就不会通过条件编译
-
#ifdef, #ifndef可以判断这个宏是否被定义
条件编译的作用一般是:
-
为了支持跨平台编译
-
排除一些调试代码
其他指令
#error 指令
如果预处理器遇到一个#error
指令,它会显示一个出错消息,大多数编译器会立即终止编译。
#error You can not include this file
#pragma指令
#pragma
指令为要求编译器执行某些特殊操作提供了一种方法。
使用#pragma pack预处理指令来设置字节对齐。具体用法如:
#pragma pack(push) // 保存现在的字节对齐状态
#pragma pack(4) // 设置4字节对齐
// 这里定义的结构体最好以4字节对齐
#pragma pack(pop) // 恢复字节对齐状态
这里字节对齐的意思是,将结构体中最大内置类型的成员的长度与默认字节对齐数(比如是4)对比,如果谁小,那么就按谁来对齐。