『C Programming』 Preprocessing Directive

『C程序设计』 预处理命令

Posted by Coekjan on March 28, 2021

本文参考自 C in a Nutshell: The Definitive Reference.

从源代码到可执行文件

其中, 在编译阶段, 有一个很重要的工作 - 执行预处理命令, 展开宏调用. 预处理命令:

  • 可以将别的源代码插入到所指定的位置
  • 可以标识出只有在特定条件下才会被编译的某一段程序代码
  • 可以定义类似标识符功能的宏

预处理命令以 # 开头, 只有空格与水平制表符可以出现在 # 之前. 命令在遇到第一个换行符后结束, 但这并不意味着命令只能写在一行内:

1
2
#define LONG_LONG A long, \
    long macro replacement value

C 编译器将把代码中所有的 "\\\n" 替换成一个空格 " "

插入头文件

插入头文件有两种格式:

1
2
#include <file-name>
#include "file-name"

如果需要插入的是标准头或实现版本所提供的其他头文件, 则应当使用第一种格式; 如果需要插入的是针对程序开发的源文件, 应当使用第二种格式.

对于尖括号内的文件, 预处理器将在系统目录下寻找文件; 对于双引号内的文件, 预处理器将先在当前目录下寻找, 再到系统目录下寻找文件.

#include 命令中也可以使用宏, 如:

1
2
3
4
5
6
#ifdef _DEBUG_
    #define SEL_HEADER "myprj_dbg.h"
#else
    #define SEL_HEADER "myprj.h"
#endif
#include SEL_HEADER

嵌套 #include

#include 命令可以嵌套使用: 通过 #include 命令插入的源文件本身可以包含另一个 #include (预处理器最多允许15层嵌套).

然而若某文件被重复包含, 可能会引发一些问题, 这可以通过条件编译命令来避免多次包含同一文件:

1
2
3
4
5
6
#ifndef INCFILE_H
#define INCFILE_H

// content

#endif

在C语言中, 可以采用 #define 命令定义宏. 宏允许将一个名称指定为任何所需的文本. 定义宏后, 无论宏名称出现在源代码的何处, 预处理器都将把它用定义时指定的文本替换掉.

字符串内部的宏名称不会被替换, 因为整个字符串是一个预处理器记号.

无参数的宏

替换文本前后的空格不属于替换文本中的内容. 下面是一些例子:

1
2
3
#define TITLE "*** Examples of Macros Without Parameters ***"
#define BUFFER_SIZE (4 * 512)
#define RANDOM (-1.0 + 2.0 * (double)rand() / RAND_MAX)

如果编写的宏包含了一个含操作数的表达式, 则应当把表达式放在圆括号内.

带参数的宏

可以定义带有形参的宏. 当预处理器展开这些宏时, 先使用调用宏时指定的实参取代替换文本中的形参. 这样的宏称为类函数宏. 定义这样的宏时, 必须确保宏名称与左圆括号间没有空白符.

可选参数

C99开始, 允许定义带有省略号的宏, 省略号需要放在参数列表的后面, 表示可选参数.

当调用可选参数宏时, 预处理器会将所有可选参数连同分割它们的逗号打包作为一个参数. 在替换文本中, 标识符 __VA_ARGS__ 对应一组前述打包的可选参数. 标识符 __VA_ARGS__ 只能用在宏定义时的替换文本中. 下面是一个例子:

1
2
3
#define printLog(...) fprintf(fp_log, __VA_ARGS__)

printLog("%s: intVar = %d\n", __func__, intVar);

预处理器将把最后一行的宏调用替换为:

1
fprintf(fp_log, "%s: intVar = %d\n", __func__, intVar);

预定义标识符 __func__ 可以在任意函数内使用, 表示当前函数名的字符串.

字符串化运算符

宏的替换文本中, 一元运算符 # 常称作字符串化运算符, 它会把宏调用时的实参转换为字符串. # 的操作数必须是宏替换文本中的形参: 当形参名称出现在替换文本中且具有前缀 # 时, 预处理器将把该形参对应的实参放到一对双引号中, 形成一个字符串字面量. 实参的所有字符本身维持不变, 但有下述例外:

  • 在实参各记号之间如果存在空白符序列, 则都会被替换为一个空格.
  • 实参的每个双引号 " 的前面都会添加一个反斜线 \
  • 实参的字符常量, 字符串字面量中的每一个反斜线前面, 也会添加一个反斜线. 但如果该反斜线本身就是通用字符名的一部分, 就不会在其前面添加反斜线.

考虑如下例子:

1
2
3
#define printDBL(exp) printf(#exp " = %f", exp)

printDBL(4 * atan(1.0));

宏调用将展开为:

1
printf("4 * atan(1.0)" " = %f", 4 * atan(1.0));

由于编译器会合并紧邻的字符串字面量, 于是上述代码进一步被转化为:

1
2
printf("4 * atan(1.0) = %f", 4 * atan(1.0));
// output : 4 * atan(1.0) = 3.141593

下面的例子展示 # 如何修改实参的空白符, 双引号, 反斜线:

1
2
#define showArgs(...) puts(#__VA_ARGS__)
showArgs( one\n,              "2\n", three);

预处理器将使用如下文本替换该宏调用:

1
puts("one\n, \"2\\n\", three");

记号粘贴运算符

宏替换文本中, 二元运算符 ## 常称作记号粘贴运算符, 它会把左右操作符结合, 作为一个记号. 如果结果文本中仍含有宏文本, 则预处理器将继续进行宏替换. 出现在 ## 前后的空白符连同 ## 本身也一并被删除.

通常使用 ## 时, 至少有一个操作数是宏的形参, 这种情况下, 实参值会先替换形参, 然后等记号粘贴完成后才进行宏展开:

1
2
3
#define TEXT_A "Hello!"
#define msg(x) puts(TEXT_ ## x)
msg(A);

展开过程分两步:

1
2
3
4
// step 1
puts(TEXT_A);
// step 2
puts("Hello!");

层次化展开队列与宏的延时展开技术

替换实参, 执行完 # , ## 后, 预处理器将检查得到的文本, 并展开包含其中的所有宏.

展开宏的过程是层次化展开队列, 本质上是广度优先搜索的过程. 这一点尤为重要, 它为宏的延时展开提供了依据.

考虑如下的宏定义:

1
2
3
#define STR(s) #s
#define FUNC(x) (cos(x))
STR(FUNC(1.0))

由于宏展开不是深度优先搜索, 因此首先展开 STR 宏. 最后一行宏调用将一步展开为:

1
"FUNC(1.0)"

但本意是将 FUNC(1.0) 的展开结果字符串化, 要实现本意就需要引入宏的延时展开技术:

1
2
3
4
#define XSTR(s) #s
#define STR(s) XSTR(s)
#define FUNC(x) (cos(x))
STR(FUNC(1.0))

此时展开的过程如下所示:

1
2
3
4
// step 1
XSTR((cos(1.0)))
// step 2
"(cos(1.0))"

泛型宏

C11开始, 可以使用泛型选择. 这意味着程序员可以定义自己的泛型宏. 泛型选择以关键字 _Generic 开头, 下面的例子展示了头文件 tgmath.h 里泛型宏 log10(x) 一种可能的实现:

1
2
3
4
5
#define log10(x) _Generic((x), \
    long double:    log10l, \
    float:          log10f, \
    default:        log10 \
    )(x)

编译器依据表达式 x 的类型, 选择 log10l , log10f , log10 中的一个.

条件编译

条件编译命令指定预处理器根据特定条件判定保留或删除某段源代码. 例如可以使用条件编译让源代码适用于不同的目标系统, 而不需要管理源代码的不同版本.

条件编译区以 #if , #ifdef , #ifndef 等命令作为开头, 以 #endif 作为结尾. 其中可以包含任意数量的 #elif 命令, 但最多一个 #else 命令:

#if#elif 命令

#if#elif 的条件表达式, 必须是整数常量预处理器表达式:

  • 不能在 #if#elif 中使用类型转换运算符
  • 可以使用预处理运算符 defined
  • 在预处理器展开所有宏, 计算完所有 defined 表达式后, 会使用字符 0 替换表达式中所有其他标识符或关键字
  • 表达式中所有带符号值都具有 intmax_t 类型, 无符号值都具有uintmax_t 类型
  • 预处理器会把字符常量和字符串字面量中的字符和转义序列转换成运行字符集中对应的字符.

defined 运算符

一元运算符 defined 可以出现在 #if#elif 命令条件中

其操作数是一个已定义的宏名称, 则生成 1 , 否则生成 0.

下面是一个使用 defined 的实例:

1
2
3
#if defined(__unix__) && defined(__GNUC__)
// ...
#endif

#ifdef#ifndef 命令

1
2
3
4
5
6
7
#ifdef ID
// equals
#if define ID
/* ---------- */
#ifndef ID
// equals
#if !define ID

行号定义

编译器会在警告信息, 错误信息与调试信息中包含代码所在的行号与所在的源文件名. 可以在源文件中利用 #line 命令指定文件名与行号信息:

1
#line line_number "filename"

如:

1
#line 1200 "primary.c"

程序可以使用标准预定义宏 __LINE____FILE__ 访问当前行号与文件名设置:

1
printf("The message was printed by line %d in the file %s.\n", __LINE__, __FILE__);

生成错误

可以使用 #error 命令让预处理器发出错误信息, 如:

1
2
3
#ifndef __STDC__
    #error "The compiler does not conform to the ANSI C standard."
#endif

#pragma 命令

#pragma 是向编译器提供额外信息的标准方法. 如:

1
2
3
#if defined(__GNUC__) && defined(_MSC_VER)
    #pragma pack(1)
#endif

使得编译器让结构成员对齐到字节边界(无填充).

为了使得程序具有良好的可移植性, 应当尽可能少用或不用 #pragma 命令.

需要注意的是, #pragma pack(n) 仅仅只是告诉编译器最多对齐到 n 字节, 并不是要求必须对齐到 n 字节.

_Pragma 运算符

宏展开不能创建 #pragma 命令. 当遇到需要这么做时, 可以使用C99新增的预处理运算符 _Pragma , 它可以配合宏使用:

1
_Pragma (string_literal)

操作数会被”解字符串化”, 或被转换为预处理记号序列, 其过程如下:

  1. 删除字符串前后的双引号
  2. 使用 " 替代 \" , 使用 \ 替代 \\

下面是一个例子:

1
2
3
#define STR(x) #x
#define ALIGN(x) _Pragma(STR(pack(x)))
ALIGN(2)

宏展开过程如下:

1
2
3
4
// step 1
_Pragma(STR(pack(2)))
// step 2
_Pragma("pack(2)")

随后预处理器将 _Pragma#pragma 命令:

1
#pragma pack(2)

预定义宏

标准预定义宏

__DATE__ : 其替换文本是编译日期的字符串字面量, 日期格式为 Mmm dd yyyy . 若天数少于 10 , 就在其前面添加一个空格符.

__FILE__ : 一个含有当前源文件名的字符串字面量.

__LINE__ : 一个整数常量, 其值是行号.

__TIME__ : 一个包含编译时间的字符串字面量, 格式为 hh:mm:ss .

__STDC__ : 整数常量 1 , 表示该编译器遵循ISO C标准.

__STDC_HOSTED__ : 如果当前实现版本为宿主环境下的实现版本, 则该宏为整数常量 1 ; 否则为 0 .

__STDC_VERSION__ : 如果编译器支持1999年1月的C99标准, 则该宏为长整数常量 199901L ; 如果编译器支持2011年12月的C11标准, 则该宏为长整数常量 201112L .

条件预定义宏

__STDC_IEC_559__ : 如果该实现版本的浮点实数算术符合IEC 60559标准, 则该常量值为 1 .

__STDC_IEC_559_COMPLEX__ : 如果该实现版本的浮点复数算术符合IEC 60559标准, 则该常量值为 1 .

__STDC_ISO_10646__ : 该长整数常量代表 yyyymmL 格式的一个日期(如 199712L) . 如果该常量值被定义, 那么类型为 wchar_t 的宽字符编码符合 ISO/IEC 10646 标准, 且符合包含到该宏所定义日期位置所制定的所有增补和修订.

C11下的可选宏

__STDC_MB_MIGHT_NEQ_WC__ : 如果一个在基本字符集中的字符, 不强制要求它以 wchar_t 对象的编码值等于它对应的字符常量, 则该常量值为 1 .

__STDC_UTF_16__ : 如果类型 char16_tutf-16 编码, 则该常量值为 1 ; 如果该类型使用其他编码, 则该宏未定义.

__STDC_UTF_32__ : 如果类型 char32_tutf-32 编码, 则该常量值为 1 ; 如果该类型使用其他编码, 则该宏未定义.

__STDC_ANALYABLE__ : 如果当前版本支持C11标准中附录L中对运行错误的分析, 则该常量值为 1 .

__STDC_LIB_EXT1__ : 如果当前实现版本支持C11标准中附录K中关于边界检查的新函数, 则该常量值为 201112L . 这些系函数名称均以 _s 结尾.

__STDC_NO_ATOMICS__ : 如果当前实现版本没有包括针对源自内存访问操作的类型与函数(不存在头文件 stdatomic.h) , 则该常量值为 1 .

__STDC_NO_COMPLEX__ : 如果当前实现版本不支持复数算术运算(不存在头文件 complex.h) , 则该常量值为 1 .

__STDC_NO_THREADS__ : 如果当前实现版本不支持多线程(不存在头文件 threads.h) , 则该常量值为 1 .

__STDC_NO_VLA__ : 如果当前实现版本不支持可变长度数组 , 则该常量值为 1 .

不可以使用 #define#undef 使用本节所介绍的预定义宏. 最后, 宏 __cplusplus 专为 C++ 编译器保留, 因此编译C源文件时, 不能定义名为 __cplusplus 的宏.