跳转至

第十四章 预处理器


预处理器的工作方式

预处理器的行为是由指令控制的。这些指令是由#字符开头的一些命令。比如 #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;           \
};

这个宏定义定义了一个取最大值的函数,可以方便的为这个函数指定比较类型。

值得注意的是,#和##都在简单的宏替换后起作用

宏定义中的圆括号

如果宏定义的替换列表是一个表达式,那么为其增加圆括号是必不可少的工作。

这是因为如果不加圆括号,在宏替换后,新的表达式可能会破坏替换列表表达式的运算优先级。

在替换列表表达式中使用圆括号有两条规则:

  1. 用圆括号将替换列表括起来
  2. 用圆括号把每个宏参数括起来

一个安全的宏的例子:

#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)对比,如果谁小,那么就按谁来对齐。