在程序被编译之前,它实际上会先经过一个叫作预处理的阶段。这几乎就像在 C 语言之上还有一门语言先运行一样。它输出 C 代码,然后这些代码再被编译。
我们已经在一定程度上通过 #include 见过它了!那就是 C 预处理器!它在看到该指令的地方,把指定的文件就地包含进来,就像你在那里亲手敲进去一样。然后编译器才构建整个东西。
但事实证明,它的能力远不止包含东西。你可以定义会被替换的宏……甚至还能定义带参数的宏!
#include我们就从已经见过很多次的这个开始。这当然是一种把其他源文件包含进你的源文件的方式。它非常常用于头文件。
虽然规范允许 #include 有各种行为,我们还是要采取更务实的做法,谈谈它在我见过的每个系统上的工作方式。
我们可以把头文件分成两类:系统头文件和本地头文件。像 stdio.h、stdlib.h、math.h 等内置头文件,可以用尖括号包含:
#include <stdio.h>
#include <stdlib.h>尖括号是在告诉 C:“嘿,别在当前目录里找这个头文件——去系统级的 include 目录里找。”
当然,这也就意味着必定有一种方式可以包含当前目录里的本地文件。确实有:用双引号:
#include "myheader.h"或者你很可能还可以用正斜杠和点号去相对目录里查找,像这样:
#include "mydir/myheader.h"
#include "../someheader.py"在 #include 里不要用反斜杠(\)作路径分隔符!那是未定义行为!只用正斜杠(/),即使在 Windows 上也是如此。
总结一下:系统头文件用尖括号(< 和 >),你自己的头文件用双引号(")。
宏是一个标识符,在编译器还没看到代码之前就会被展开成另一段代码。可以把它想成占位符——当预处理器看到这些标识符之一时,就把它替换成你定义的另一个值。
我们用 #define 来做这件事(通常读作“pound define”,或者“hash define”,但极少——如果有的话——读作“octothorpe define”)。举个例子:
#include <stdio.h>
#define HELLO "Hello, world"
#define PI 3.14159
int main(void)
{
printf("%s, %f\n", HELLO, PI);
}在第 3 行和第 4 行,我们定义了两个宏。它们在代码中其他地方(第 8 行)出现时,都会被替换成所定义的值。
从 C 编译器的角度看,就完全等同于我们写成了这样:
看到 HELLO 被替换成 "Hello, world"、PI 被替换成 3.14159 了吗?从编译器的角度看,就好像那些值本来就出现在代码的那个位置。
注意,宏本身并没有特定的类型。实际上发生的一切只是:它们被整体替换成 #define 所定义的内容。如果得到的 C 代码不合法,编译器就会吐出来。
你也可以定义一个没有值的宏:
#define EXTRA_HAPPY在这种情况下,宏存在且已被定义,但被定义为空。所以它在文本中出现的任何地方都会被替换成空。稍后我们会看到它的用途。
按惯例,宏名要写成 ALL_CAPS,尽管技术上并不要求这样。
总的来说,这给了你一种定义常量值的方式,这些常量实际上是全局的,可以在任何地方使用。即使在 const 变量行不通的地方也可以,比如 switch 的 case 里和固定数组长度中。
话虽如此,网上一直在激烈争论:在一般情况下,带类型的 const 变量是否比 #define 宏更好。
它还可以用来替换或修改关键字,这对 const 来说是全然陌生的概念,不过这种用法应当少用。
可以让预处理器决定是否把某些代码块呈现给编译器,或者在编译之前干脆把它们整个删掉。
做法基本上就是把代码包进条件块里,类似于 if-else 语句。
#ifdef 与 #endif首先,我们来试试根据某个宏是否被定义来编译特定的代码。
#include <stdio.h>
#define EXTRA_HAPPY
int main(void)
{
#ifdef EXTRA_HAPPY
printf("I'm extra happy!\n");
#endif
printf("OK!\n");
}在那个示例中,我们定义了 EXTRA_HAPPY(定义为空,但它确实被定义了),然后在第 8 行用 #ifdef 指令检查它是否已定义。如果已定义,后续代码就会被包含进来,直到 #endif 为止。
所以,因为它已被定义,这段代码就会被纳入编译,输出将是:
I'm extra happy!
OK!如果我们把那条 #define 注释掉,像这样:
//#define EXTRA_HAPPY(已注释掉)那么它就不会被定义,代码也不会被纳入编译。输出就只是:
OK!要记住,这些决定都发生在编译期!代码实际上会根据条件被编译或被删除。这与标准的 if 语句形成对比,后者是在程序运行时求值的。
#ifndef“如果已定义”还有否定形式:“如果未定义”,即 #ifndef。我们可以改写前面的示例,让它根据某个东西是否被定义来输出不同内容:
#ifdef EXTRA_HAPPY
printf("I'm extra happy!\n");
#endif
#ifndef EXTRA_HAPPY
printf("I'm just regular\n");
#endif下一节我们会看到更干净利落的做法。
把这一切再联系回头文件:我们已经看到,如何用这样的预处理器指令把头文件包起来,从而让它只被包含一次:
#ifndef MYHEADER_H // myheader.h 的第一行
#define MYHEADER_H
int x = 12;
#endif // myheader.h 的最后一行
这演示了宏如何跨文件、跨多次 #include 持续存在。如果它还没有被定义,我们就定义它,并编译整个头文件。
但下次它被包含时,我们会看到 MYHEADER_H 确实已定义,于是我们不把头文件交给编译器——它实际上被移除了。
#else但我们能做的还不止这些!还可以把 #else 也加进来。
我们来改一下前面的示例:
现在如果 EXTRA_HAPPY 未定义,就会走到 #else 分支并打印:
I'm just regular#elifdef、#elifndef
这个特性是 C23 新增的!
不过,如果你想要更复杂的东西呢?也许你需要一个 if-else 级联结构,才能把代码正确地构建出来?
幸运的是,这些指令都可供我们使用。我们可以用 #elifdef 表示“否则如果已定义”:
#ifdef MODE_1
printf("This is mode 1\n");
#elifdef MODE_2
printf("This is mode 2\n");
#elifdef MODE_3
printf("This is mode 3\n");
#else
printf("This is some other mode\n");
#endif反过来,你可以用 #elifndef 表示“否则如果未定义”。
#if、#elif
它的工作方式和 #ifdef、#ifndef 指令非常相似:你也可以有 #else,而整个结构以 #endif 收尾。
唯一的区别是,#if 后面的常量表达式必须求值为真(非零),#if 里的代码才会被编译。所以我们关心的不是某个东西是否已定义,而是一个求值为真的表达式。
#include <stdio.h>
#define HAPPY_FACTOR 1
int main(void)
{
#if HAPPY_FACTOR == 0
printf("I'm not happy!\n");
#elif HAPPY_FACTOR == 1
printf("I'm just regular\n");
#else
printf("I'm extra happy!\n");
#endif
printf("OK!\n");
}同样,对于不匹配的 #if 分支,编译器根本看不到那些行。对于上面的代码,预处理器处理完之后,编译器看到的全部内容就是:
它的一种取巧用法是快速注释掉大量代码行128。
如果你在要注释掉的代码块开头放一个 #if 0(“如果为假”),在末尾放一个 #endif,就能得到这种效果:
#if 0
printf("All this code"); /* 实际上等同于 */
printf("commented out"); // 被 #if 0 排除掉
#endif如果你用的是 C23 之前的编译器,不支持 #elifdef 或 #elifndef 指令,那怎么办?怎么用 #if 达到同样的效果?也就是说,如果我想要下面这样:
#ifdef FOO
x = 2;
#elifdef BAR // 潜在错误:C23 之前不支持
x = 3;
#endif我该怎么做?
原来有一个叫 defined 的预处理器运算符,可以和 #if 语句一起使用。
下面这些是等价的:
#ifdef FOO
#if defined FOO
#if defined(FOO) // 括号可选下面这些也是:
#ifndef FOO
#if !defined FOO
#if !defined(FOO) // 括号可选注意,我们可以用标准的逻辑非运算符(!)来表示“未定义”。
现在我们又回到了 #if 的地盘,可以肆无忌惮地使用 #elif 了!
这段有问题的代码:
#ifdef FOO
x = 2;
#elifdef BAR // 潜在错误:C23 之前不支持
x = 3;
#endif可以替换成:
#if defined FOO
x = 2;
#elif defined BAR
x = 3;
#endif
#undef如果你定义了某个东西但不再需要它,可以用 #undef 取消定义。
#include <stdio.h>
int main(void)
{
#define GOATS
#ifdef GOATS
printf("Goats detected!\n"); // 打印
#endif
#undef GOATS // 让 GOATS 不再被定义
#ifdef GOATS
printf("Goats detected, again!\n"); // 不打印
#endif
}标准定义了许多内置宏,你可以测试它们并用于条件编译。我们在这里看看这些宏。
下面这些都已经定义:
| 宏 | 说明 |
|---|---|
__DATE__ |
编译日期——比如你编译这个文件的那一天——格式为 Mmm dd yyyy |
__TIME__ |
编译时间,格式为 hh:mm:ss |
__FILE__ |
包含本文件名的字符串 |
__LINE__ |
该宏出现处所在文件的行号 |
__func__ |
该宏所在函数的名称,以字符串形式表示129 |
__STDC__ |
如果这是标准 C 编译器,则定义为 1 |
__STDC_HOSTED__ |
如果编译器是托管实现130,则为 1,否则为 0 |
__STDC_VERSION__ |
C 的版本号,是一个形如 yyyymmL 的 long int 常量,例如 201710L |
我们把它们放到一起。
#include <stdio.h>
int main(void)
{
printf("This function: %s\n", __func__);
printf("This file: %s\n", __FILE__);
printf("This line: %d\n", __LINE__);
printf("Compiled on: %s %s\n", __DATE__, __TIME__);
printf("C Version: %ld\n", __STDC_VERSION__);
}
在我的系统上,输出是:
This function: main
This file: foo.c
This line: 7
Compiled on: Nov 23 2020 17:16:27
C Version: 201710__FILE__、__func__ 和 __LINE__ 在向开发者报告错误信息时特别有用。<assert.h> 中的 assert() 宏正是用它们来指出断言在代码中何处失败。
__STDC_VERSION__ 的版本值如果你好奇的话,下面是 C 语言规范各个主要发行版对应的版本号:
| 发行版 | ISO/IEC 版本 | __STDC_VERSION__ |
|---|---|---|
| C89 | ISO/IEC 9899:1990 版 | 未定义 |
| C89 | ISO/IEC 9899:1990/Amd.1:1995 版 | 199409L |
| C99 | ISO/IEC 9899:1999 版 | 199901L |
| C11 | ISO/IEC 9899:2011/Amd.1:2012 版 | 201112L |
注意,这个宏最初在 C89 中并不存在。
还要注意,按照计划,版本号会严格递增,所以你总是可以这样检查,比如说“至少是 C99”:
#if __STDC_VERSION__ >= 1999901L
你的实现也可能定义这些宏。也可能不定义。
| 宏 | 说明 |
|---|---|
__STDC_ISO_10646__ |
如果已定义,wchar_t 保存的是 Unicode 值,否则是别的什么 |
__STDC_MB_MIGHT_NEQ_WC__ |
1 表示多字节字符中的值可能无法与宽字符中的值一一对应 |
__STDC_UTF_16__ |
1 表示系统在 char16_t 类型中使用 UTF-16 编码 |
__STDC_UTF_32__ |
1 表示系统在 char32_t 类型中使用 UTF-32 编码 |
__STDC_ANALYZABLE__ |
1 表示代码是可分析的131 |
__STDC_IEC_559__ |
如果支持 IEEE-754(即 IEC 60559)浮点数,则为 1 |
__STDC_IEC_559_COMPLEX__ |
如果支持 IEC 60559 复数浮点数,则为 1 |
__STDC_LIB_EXT1__ |
如果本实现支持各种“安全”的替代标准库函数(函数名带有 _s 后缀),则为 1 |
__STDC_NO_ATOMICS__ |
如果本实现不支持 _Atomic 或 <stdatomic.h>,则为 1 |
__STDC_NO_COMPLEX__ |
如果本实现不支持复数类型或 <complex.h>,则为 1 |
__STDC_NO_THREADS__ |
如果本实现不支持 <threads.h>,则为 1 |
__STDC_NO_VLA__ |
如果本实现不支持变长数组,则为 1 |
不过,宏的能力不止于简单替换。你还可以让它们接收参数,并把参数替换进去。
经常有人问,什么时候该用带参数的宏,什么时候该用函数。简短回答:用函数。但你在实际项目和标准库里会看到大量宏。人们倾向于把它们用于简短的数学式操作,也用于那些可能因平台而异的功能。你可以为这个平台或那个平台定义不同的关键字。
我们先从一个简单的例子开始,它求一个数的平方:
#include <stdio.h>
#define SQR(x) x * x // 不太对,但先听我说下去
int main(void)
{
printf("%d\n", SQR(12)); // 144
}它说的是:“凡是你看到带某个值的 SQR,就把它替换成那个值乘以它自己。”
于是第 7 行会变成:
C 会轻轻松松地把它转换成 144。
但我们在那个宏里犯了一个低级错误,一个必须避免的错误。
我们来看看。如果我们要计算 SQR(3 + 4) 呢?嗯,\(3+4=7\),所以我们想要计算的当然是 \(7^2=49\)。就是这样;49——最终答案。
把它放进代码里,看看我们得到……19?
发生了什么?
如果顺着宏展开走一遍,我们得到
哎呀!因为乘法优先级更高,我们先算 \(4\times3=12\),得到 \(3+12+4=19\)。这不是我们想要的结果。
所以我们必须修好它,才能得到正确结果。
这种情况太常见了,所以每次编写带参数的数学宏时,你都应该下意识地这样做!
修起来很简单:加些括号就行!
现在我们的宏展开为:
但实际上我们仍然有同样的问题,如果附近有一个优先级比乘法(*)更高的运算符,它就可能显现出来。
所以,安全、正确的宏写法是把整个东西再用括号包一层,像这样:
写数学宏时养成这个习惯,你就不会出错。
这些东西你想叠多少层就叠多少层:
#define TRIANGLE_AREA(w, h) (0.5 * (w) * (h))我们来写几个用求根公式解 \(x\) 的宏。万一你不能把它背得滚瓜烂熟,它是说,对于这种形式的方程:
\(ax^2+bx+c=0\)
你可以用求根公式解出 \(x\):
\(x=\displaystyle\frac{-b\pm\sqrt{b^2-4ac}}{2a}\)
这太疯狂了。还要注意其中有个正负号(\(\pm\)),表示实际上有两个解。
那我们给两个解都写宏:
#define QUADP(a, b, c) ((-(b) + sqrt((b) * (b) - 4 * (a) * (c))) / (2 * (a)))
#define QUADM(a, b, c) ((-(b) - sqrt((b) * (b) - 4 * (a) * (c))) / (2 * (a)))这样我们就有了点数学。但我们再定义一个,可以用作 printf() 的实参来打印两个答案。
// 宏 替换内容
// |-----------| |----------------------------|
#define QUAD(a, b, c) QUADP(a, b, c), QUADM(a, b, c)那只是用逗号分隔的两个值——我们可以把它当作某种“合并”的实参传给 printf(),像这样:
printf("x = %f or x = %f\n", QUAD(2, 10, 5));我们把它组合成一段代码:
#include <stdio.h>
#include <math.h> // 用于 sqrt()
#define QUADP(a, b, c) ((-(b) + sqrt((b) * (b) - 4 * (a) * (c))) / (2 * (a)))
#define QUADM(a, b, c) ((-(b) - sqrt((b) * (b) - 4 * (a) * (c))) / (2 * (a)))
#define QUAD(a, b, c) QUADP(a, b, c), QUADM(a, b, c)
int main(void)
{
printf("2*x^2 + 10*x + 5 = 0\n");
printf("x = %f or x = %f\n", QUAD(2, 10, 5));
}这样得到的输出是:
2*x^2 + 10*x + 5 = 0
x = -0.563508 or x = -4.436492把其中任何一个值代进去,结果都大致为零(有点偏差,因为这些数并不精确):
\(2\times-0.563508^2+10\times-0.563508+5\approx0.000003\)
还有一种办法可以让传给宏的实参数量可变:在已知的具名参数后面使用省略号(...)。宏展开时,所有额外的实参都会以逗号分隔的列表形式出现在 __VA_ARGS__ 宏里,可以从那里替换:
#include <stdio.h>
// 把前两个实参合并成一个数字,
// 然后剩下的实参构成一个逗号列表:
#define X(a, b, ...) (10*(a) + 20*(b)), __VA_ARGS__
int main(void)
{
printf("%d %f %s %d\n", X(5, 4, 3.14, "Hi!", 12));
}第 10 行发生的替换会是:
输出为:
130 3.140000 Hi! 12你还可以在 __VA_ARGS__ 前面加一个 #,把它“字符串化”:
#define X(...) #__VA_ARGS__
printf("%s\n", X(1,2,3)); // 打印 "1, 2, 3"
上面刚提到过,你可以在替换文本中给任何实参加上一个 # 前缀,把它变成字符串。
例如,我们可以用这个宏配合 printf() 把任何东西当作字符串打印出来:
#define STR(x) #x
printf("%s\n", STR(3.14159));在这种情况下,替换结果是:
printf("%s\n", "3.14159");我们看看能不能把这招发挥得更好:把任意 int 变量名传进宏里,让它打印出该变量的名字和值。
#include <stdio.h>
#define PRINT_INT_VAL(x) printf("%s = %d\n", #x, x)
int main(void)
{
int a = 5;
PRINT_INT_VAL(a); // 打印 "a = 5"
}在第 9 行,我们得到如下宏替换:
我们还可以用 ## 把两个实参连接在一起。真有意思!
#define CAT(a, b) a ## b
printf("%f\n", CAT(3.14, 1592)); // 3.141592如果用反斜杠(\)转义换行符,就可以让宏延续到多行。
我们来写一个多行宏,它打印从 0 到传入的两个实参之积的数字。
#include <stdio.h>
#define PRINT_NUMS_TO_PRODUCT(a, b) do { \
int product = (a) * (b); \
for (int i = 0; i < product; i++) { \
printf("%d\n", i); \
} \
} while(0)
int main(void)
{
PRINT_NUMS_TO_PRODUCT(2, 4); // 输出 0 到 7 的数字
}这里要注意几点:
do-while(0) 循环里。后一点可能有点奇怪,但这全是为了吸收程序员在宏后面留下的那个 ;。
起初我以为只用花括号就够了,但如果程序员在宏后面加了分号,有一种情况会失败。就是这种情况:
#include <stdio.h>
#define FOO(x) { (x)++; }
int main(void)
{
int i = 0;
if (i == 0)
FOO(i);
else
printf(":-(\n");
printf("%d\n", i);
}看起来够简单,但构建时会报语法错误:
foo.c:11:5: error: ‘else’ without a previous ‘if’ 看出来了吗?
我们看看展开结果:
if (i == 0) {
(i)++;
}; // <-- 大写的 T 意味着麻烦!
else
printf(":-(\n");; 终结了 if 语句,于是 else 就非法地悬空在那里132。
所以要把那个多行宏用 do-while(0) 包起来。
在代码里加断言是捕获那些你认为不该发生的情况的好方法。C 提供了 assert() 功能。它检查一个条件,如果条件为假,程序就会崩溃退出,并告诉你断言失败的文件和行号。
但这还不够。
第一个问题可以用宏来解决。
基本上,当我有这段代码时:
ASSERT(x < 20, "x must be under 20");我希望发生类似这样的事(假设这个 ASSERT() 位于 foo.c 的第 220 行):
if (!(x < 20)) {
fprintf(stderr, "foo.c:220: assertion x < 20 failed: ");
fprintf(stderr, "x must be under 20\n");
exit(1);
}我们可以从 __FILE__ 宏取得文件名,从 __LINE__ 取得行号。消息本身已经是字符串,但 x < 20 不是,所以我们得用 # 把它字符串化。在行末使用反斜杠转义,就能写出多行宏。
#define ASSERT(c, m) \
do { \
if (!(c)) { \
fprintf(stderr, __FILE__ ":%d: assertion %s failed: %s\n", \
__LINE__, #c, m); \
exit(1); \
} \
} while(0)(__FILE__ 那样放在前面看起来有点怪,但记住它是一个字符串字面量,而相邻的字符串字面量会自动拼接起来。另一方面,__LINE__ 就只是一个 int。)
这就行了!如果我运行这个:
int x = 30;
ASSERT(x < 20, "x must be under 20");会得到这样的输出:
foo.c:23: assertion x < 20 failed: x must be under 20
非常不错!
剩下的唯一一件事就是给它加个开关,而这可以用条件编译来实现。
下面是完整的示例:
#include <stdio.h>
#include <stdlib.h>
#define ASSERT_ENABLED 1
#if ASSERT_ENABLED
#define ASSERT(c, m) \
do { \
if (!(c)) { \
fprintf(stderr, __FILE__ ":%d: assertion %s failed: %s\n", \
__LINE__, #c, m); \
exit(1); \
} \
} while(0)
#else
#define ASSERT(c, m) // 未启用时为空宏
#endif
int main(void)
{
int x = 30;
ASSERT(x < 20, "x must be under 20");
}它的输出是:
foo.c:23: assertion x < 20 failed: x must be under 20#error 指令这个指令会让编译器一看到它就报错。
它通常用在条件块里,以便在未满足某些前提条件时阻止编译:
#ifndef __STDC_IEC_559__
#error I really need IEEE-754 floating point to compile. Sorry!
#endif
有些编译器有一个非标准的配套指令 #warning,它会输出警告但不停止编译,不过 C11 规范里没有它。
#embed 指令C23 新增!
而且目前在我所有的编译器上都还不管用,所以这一节请打个折扣看!
它的要点是:你可以把某个文件的字节作为整型常量包含进来,就像你亲手把它们敲进去一样。
例如,如果你有一个名为 foo.bin 的二进制文件,其中包含四个字节,十进制值分别为 11、22、33 和 44,然后你这样写:
int a[] = {
#embed "foo.bin"
};那就完全等同于你敲入了这些:
int a[] = {11,22,33,44};这是一种非常强大的方式,可以用二进制数据初始化数组,而无需先把它们全部转换成代码——预处理器会替你做这件事!
更典型的用例或许是一个包含小图片的文件,你希望显示它,但又不想在运行时加载。
再看一个例子:
int a[] = {
#embed <foo.bin>
};如果用尖括号,预处理器会在一系列由实现定义的位置查找该文件,就像 #include 会做的那样。如果用双引号但没找到资源,编译器会退而像你用了尖括号那样再试一次,作为寻找文件的最后努力。
#embed 的工作方式和 #include 类似:它实际上会在编译器看到这些值之前就把它们粘贴进去。这意味着你可以在各种地方使用它:
return
#embed "somevalue.dat"
;
或者
int x =
#embed "xvalue.dat"
;
那么——这些总是字节吗?也就是说,它们的值会落在 0 到 255(含)之间吗?答案在默认情况下肯定是“是”,除非是“否”。
严格说来,元素宽度是 CHAR_BIT 位。而在你的系统上这很可能就是 8,所以你的值会落在 0-255 这个范围。(它们永远是非负的。)
另外,实现也有可能允许以某种方式覆盖这一点,比如在命令行上或用参数。
文件大小(以位计)必须是元素大小的整数倍。也就是说,如果每个元素是 8 位,那么文件大小(以位计)必须是 8 的倍数。在日常用法中,这不过是用一种让人困惑的方式说,每个文件的大小都得是整数个字节……而这当然总是成立的。老实说,我都不确定自己为什么要写这一段。如果你真的那么好奇,就去读规范吧。
#embed 参数你可以给 #embed 指令指定各种各样的参数。下面是一个使用尚未介绍的 limit() 参数的例子:
int a[] = {
#embed "/dev/random" limit(5)
};但如果你已经在别处定义了 limit 呢?幸运的是,你可以在关键字两边加上 __,效果完全一样:
int a[] = {
#embed "/dev/random" __limit__(5)
};那么……这个 limit 又是什么东西?
limit() 参数你可以用这个参数指定要嵌入的元素数量上限。
这是一个最大值,不是绝对值。如果要嵌入的文件比指定的上限短,那么只会导入它实际拥有的那些字节。
上面那个 /dev/random 的例子就体现了这种做法的动机——在 Unix 中,它是一个字符设备文件,会返回无穷无尽、相当随机的数字流。
嵌入无穷多个字节会把你的内存搞垮,所以 limit 参数给了你一个在达到某个数量后停下来的办法。
最后,如果你好奇的话:limit 里允许使用 #define 宏。
if_empty 参数这个参数用来定义当文件存在但不含数据时,嵌入结果应该是什么。假设文件 foo.dat 只含一个字节,值为 123。如果我们这样写:
int x =
#embed "foo.dat" if_empty(999)
;我们会得到:
int x = 123; // 当 foo.dat 中含有一个 123 字节时但如果 foo.dat 的长度为零字节(即不含数据、是空的)呢?在这种情况下,它会展开为:
int x = 999; // 当 foo.dat 为空时特别要注意,如果 limit 被设为 0,那么 if_empty 就总会被替换上来。也就是说,零上限实际上意味着文件是空的。
无论 foo.dat 里有什么,下面这个都总会输出 x = 999:
int x =
#embed "foo.dat" limit(0) if_empty(999)
;prefix() 和 suffix() 参数
这是一种在嵌入内容前面追加数据的方式。
注意,它们只对非空数据起作用!如果文件是空的,prefix 和 suffix 都不会生效。
下面这个例子嵌入三个随机数,但在这些数前面加上 11,,后面加上 ,99:
int x[] = {
#embed "/dev/urandom" limit(3) prefix(11,) suffix(,99)
};示例结果:
int x[] = {11,135,116,220,99};并不要求 prefix 和 suffix 必须同时使用。你可以两个都用、只用一个、用另一个,或者都不用。
我们可以利用“它们只作用于非空文件”这一特性来达到巧妙的效果,如下面这个厚颜无耻地从规范里抄来的例子所示。
假设我们有一个 foo.dat 文件,里面有些数据。我们想用它来初始化一个数组,然后希望在数组末尾附加一个零元素作为后缀。
没问题,对吧?
int x[] = {
#embed "foo.dat" suffix(,0)
};如果 foo.dat 里是 11、22 和 33,我们会得到:
int x[] = {11,22,33,0};但是等等!如果 foo.dat 是空的呢?那我们得到:
int x[] = {};这可不妙。
但我们可以这样修好它:
int x[] = {
#embed "foo.dat" suffix(,)
0
};由于文件为空时会省略 suffix 参数,这就会变成:
int x[] = {0};这样就没问题了。
__has_embed() 标识符这是测试某个特定文件是否可供嵌入、以及它是否为空的好方法。
你把它和 #if 指令一起使用。
下面这段代码会从随机数生成器字符设备获取 5 个随机数。如果该设备不存在,它会试着从文件 myrandoms.dat 获取。如果那个文件也不存在,它就使用一些硬编码的值:
int random_nums[] = {
#if __has_embed("/dev/urandom")
#embed "/dev/urandom" limit(5)
#elif __has_embed("myrandoms.dat")
#embed "myrandoms.dat" limit(5)
#else
140,178,92,167,120
#endif
};严格说来,__has_embed() 标识符会解析为三个值之一:
__has_embed() 结果 |
说明 |
|---|---|
__STDC_EMBED_NOT_FOUND__ |
如果文件未找到。 |
__STDC_EMBED_FOUND__ |
如果文件已找到且非空。 |
__STDC_EMBED_EMPTY__ |
如果文件已找到且为空。 |
我有充分理由相信 __STDC_EMBED_NOT_FOUND__ 是 0,而其他几个不是零(因为提案里暗示了这一点,而且逻辑上也说得通),但我在这一版的规范草案里找不到明确说法。
TODO
编译器实现想定义多少其他嵌入参数都可以——请到你所用编译器的文档里找这些非标准参数。
例如:
#embed "foo.bin" limit(12) frotz(lamp)这些参数通常可能带有前缀,以帮助划分命名空间:
#embed "foo.bin" limit(12) fmc::frotz(lamp)在使用它们之前先检测一下是否可用,也许是比较明智的,幸运的是我们可以借助 __has_embed 做到这一点。
通常,__has_embed() 只会告诉我们文件是否存在。但是——有趣的地方来了——如果任何附加参数也不被支持,它同样会返回假!
所以,如果我们给它一个我们确定存在的文件,再加上一个我们想测试其是否存在的参数,它实际上就会告诉我们该参数是否受支持。
可是哪个文件是永远存在的呢?原来我们可以用 __FILE__ 宏,它会展开成引用它的那个源文件的文件名!那个文件必定存在,否则在“先有鸡还是先有蛋”这个问题上就出了严重的问题。
我们来测试一下 frotz 参数,看看能不能用它:
#if __has_embed(__FILE__ fmc::frotz(lamp))
puts("fmc::frotz(lamp) is supported!");
#else
puts("fmc::frotz(lamp) is NOT supported!");
#endif那如果想在里面得到一些 int,而不是单个字节呢?如果嵌入文件里是多字节的值呢?
C23 标准不支持这种做法,但将来可能会为实现扩展定义相关机制。
#pragma 指令这是一条挺古怪的指令,它是“pragmatic”(务实)的缩写。你可以用它来做……嗯,做任何你的编译器支持你用它做的事。
基本上,你往代码里加它的唯一时机,就是某份文档让你这么做。
下面是一个非标准的示例:用 #pragma 让编译器以多个线程并行执行一个 for 循环(前提是编译器支持 OpenMP133 扩展):
#pragma omp parallel for
for (int i = 0; i < 10; i++) { ... }天南海北的各个角落里,都有人记录过各种各样的 #pragma 指令。
编译器会忽略所有无法识别的 #pragma。
也有几个标准的,它们以 STDC 开头,形式都一样:
#pragma STDC pragma_name on-offon-off 部分可以是 ON、OFF 或 DEFAULT。
而 pragma_name 可以是以下之一:
| pragma 名称 | 描述 |
|---|---|
FP_CONTRACT |
允许把浮点表达式收缩成单个操作,以避免多次操作可能产生的舍入误差。 |
FENV_ACCESS |
如果你打算访问浮点状态标志,就设为 ON。若为 OFF,编译器可能执行某些优化,导致标志中的值不一致或无效。 |
CX_LIMITED_RANGE |
设为 ON 可允许编译器在做复数运算时跳过溢出检查。默认为 OFF。 |
例如:
#pragma STDC FP_CONTRACT OFF
#pragma STDC CX_LIMITED_RANGE ON至于 CX_LIMITED_RANGE,规范指出:
该 pragma 的目的是允许实现使用以下公式:
乘法:\((x+iy)\times(u+iv) = (xu-yv)+i(yu+xv)\)
除法:\((x+iy)/(u+iv) = [(xu+yv)+i(yu-xv)]/(u^2+v^2)\)
模长:\(|x+iy|=\sqrt{x^2+y^2}\)
前提是程序员能判断出在这里使用它们是安全的。
_Pragma 运算符这是声明 pragma 的另一种方式,可以把它用在宏里。
下面两种写法等价:
#pragma "Unnecessary" quotes
_Pragma("\"Unnecessary\" quotes")如果需要,它可以放在宏里使用:
#define PRAGMA(x) _Pragma(#x)
#line 指令
它允许你覆盖 __LINE__ 和 __FILE__ 的值。如果你真想这么做的话。
我从没想过要这么做,不过 K&R2 里写道:
为了便于其他生成 C 程序的预处理器 […]
所以也许这就是它的用处。
要把行号覆盖成比如 300:
#line 300然后 __LINE__ 就会从那里继续往上数。
要同时覆盖行号和文件名:
#line 300 "newfilename"单独占一行的 # 会被预处理器忽略。不过老实说,我不知道这有什么实际用处。
我见过这样的例子:
#ifdef FOO
#
#else
printf("Something");
#endif这纯粹是装饰;把那个孤零零的 # 所在的行删掉,也不会有任何不良影响。
或者为了视觉上的一致性,像这样:
#
#ifdef FOO
x = 2;
#endif
#
#if BAR == 17
x = 12;
#endif
#不过,从美观的角度来说,这实在难看。
另一个帖子提到注释的消除——在 GCC 中,# 后面的注释不会被编译器看到。我并不怀疑这一点,但规范似乎并没有说这是标准行为。
我搜索理由的努力收效甚微。所以我干脆说:这就是老派的 C 冷知识。