| 目录 |

41 函数说明符、对齐说明符/运算符

以我的经验,这些用得实在不算多,不过为了完整起见,我们还是在这里讲一讲。

41.1 函数说明符

声明函数时,你可以给编译器提供几条提示,说明这些函数可能或将会如何被使用。这使编译器能够进行某些优化,或者促使它去做这些优化。

41.1.1 inline 提速——也许吧

你可以像这样把一个函数声明为 inline:

static inline int add(int x, int y) {
    return x + y;
}

这样做是为了鼓励编译器尽可能快地完成这次函数调用。而从历史上看,做到这一点的一种办法是内联,也就是把函数体完整地嵌入到发生调用的位置。这样可以避免建立函数调用和拆除调用带来的全部开销,代价则是代码体积变大,因为函数会被复制到各处,而不是被复用。

要记住的几条简单粗暴的要点是:

  1. 你多半不需要为了提速而使用 inline。现代编译器知道什么最好。

  2. 如果你确实为了提速而用它,就把它用在文件作用域上,即 static inline。这样可以避开外部链接性和内联函数那些麻烦的规则。

现在别再读这一节了。

真是个自讨苦吃的人,是吧?

我们试试把 static 去掉。

#include <stdio.h>

inline int add(int x, int y)
{
    return x + y;
}

int main(void)
{
    printf("%d\n", add(1, 2));
}

gcc 对 add()231 报出链接器错误。规范要求:如果你有一个非 extern 的内联函数,就必须再提供一个具有外部链接性的版本。

所以你得在别处再有一个 extern 版本,这段代码才能工作。如果编译器在当前文件里有一个 inline 函数,又在别处有同一函数的外部版本,它就可以自行选择调用哪一个。所以我强烈建议两者保持一致。

另一件可以做的事,是把函数声明为 extern inline。这会尝试在同一文件里内联(为了速度),但也会创建一份具有外部链接性的版本。

41.1.2 noreturn 与 _Noreturn

它向编译器表明:某个特定函数永远不会返回它的调用者,也就是说,程序会在该函数返回之前以某种方式退出。

它让编译器可以围绕该函数调用做一些优化。

它还让你可以向其他开发者表明:有些程序逻辑依赖于某个函数不返回。

你很可能永远都用不到它,但你会在一些库函数调用中看到它,比如 exit()232 和 abort()233。

内置关键字是 _Noreturn,但如果它不会破坏你现有的代码,大家都会建议你包含 <stdnoreturn.h>,并改用更易读的 noreturn。

如果一个声明为 noreturn 的函数真的返回了,那就是未定义行为。要知道,这在计算上是不诚实的。

下面是一个正确使用 noreturn 的示例:

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void foo(void) // 这个函数永远不应该返回!
{
    printf("Happy days\n");

    exit(1);            // 而它也的确不返回——它在这里退出!
}

int main(void)
{
    foo();
}

如果编译器检测到某个 noreturn 函数可能返回,它可能会好心地给你一个警告。

把 foo() 函数换成下面这样:

noreturn void foo(void)
{
    printf("Breakin' the law\n");
}

我就会得到一条警告:

foo.c:7:1: warning: function declared 'noreturn' should not return

41.2 对齐说明符与运算符

对齐234讲的是存放对象的地址可以取哪些倍数。你能把它存放在任意地址吗?还是说起始地址必须能被 2 整除?或者 8?或者 16?

如果你在编写与操作系统打交道的底层代码,比如内存分配器,就可能需要考虑这一点。大多数开发者的整个职业生涯都没在 C 里用过这项功能。

41.2.1 alignas 与 _Alignas

这不是函数,而是一个对齐说明符,可以用在变量声明中。

内置说明符是 _Alignas,但头文件 <stdalign.h> 把它定义成 alignas,看起来更顺眼。

如果你需要让自己的 char 像 int 那样对齐,可以在声明时这样强制指定:

char alignas(int) c;

你也可以传入一个常量值或表达式作为对齐值。这必须是系统支持的值,但规范并未明确规定你能填哪些值。小的 2 的幂(1、2、4、8 和 16)通常比较保险。

char alignas(8) c;   // 按 8 字节边界对齐

如果你想按系统所用到的最大对齐值来对齐,就包含 <stddef.h> 并使用 max_align_t 类型,像这样:

char alignas(max_align_t) c;

你也可以指定比 max_align_t 更大的对齐值,从而过度对齐,但这样是否被允许取决于系统。

41.2.2 alignof 与 _Alignof

这个运算符会返回某个类型在当前系统上用于对齐的地址倍数。例如,也许 char 每隔 1 个地址对齐,而 int 每隔 4 个地址对齐。

内置运算符是 _Alignof,但如果你想让代码看起来更酷,头文件 <stdalign.h> 把它定义成了 alignof。

下面这个程序会打印出各种不同类型的对齐值。同样,这些值因系统而异。注意,max_align_t 类型会给出系统所使用的最大对齐值。

#include <stdalign.h>
#include <stdio.h>     // 用于 printf()
#include <stddef.h>    // 用于 max_align_t

struct t {
    int a;
    char b;
    float c;
};

int main(void)
{
    printf("char       : %zu\n", alignof(char));
    printf("short      : %zu\n", alignof(short));
    printf("int        : %zu\n", alignof(int));
    printf("long       : %zu\n", alignof(long));
    printf("long long  : %zu\n", alignof(long long));
    printf("double     : %zu\n", alignof(double));
    printf("long double: %zu\n", alignof(long double));
    printf("struct t   : %zu\n", alignof(struct t));
    printf("max_align_t: %zu\n", alignof(max_align_t));
}

在我的系统上的输出:

char       : 1
short      : 2
int        : 4
long       : 8
long long  : 8
double     : 8
long double: 16
struct t   : 16
max_align_t: 16

41.3 memalignment() 函数

C23 新增!

(注意:我的编译器都还不支持这个函数,所以这段代码基本未经测试。)

如果你知道数据的类型,alignof 就非常好用。但要是你对类型 可悲地一无所知,只有一个指向该数据的指针,那该怎么办?

这怎么可能发生呢?

嗯,当然是通过我们的老朋友 void*。我们没法把它传给 alignof,但如果我们想知道它指向的东西的对齐值呢?

如果我们即将把这块内存用于对齐要求很高的东西,就可能需要知道这一点。例如,原子类型和浮点类型在未对齐时常常表现异常。

所以有了这个函数,只要我们有指向某块数据的指针,就能检查它的对齐值,即使那是个 void* 也没问题。

我们来快速测试一下,看看一个 void 指针是否对齐得足以用作原子类型;如果可以,就取一个变量,把它当作该类型来用:

void foo(void *p)
{
    if (memalignment(p) >= alignof(atomic int)) {
        atomic int *i = p;
        do_things(i);
    } else
        puts("This pointer is no good as an atomic int\n");

...

我猜你很少会(很可能永远不会)需要用到这个函数,除非你在做一些底层的东西。

就这样。对齐!


  1. https://www.ioccc.org/↩︎

  2. https://en.wikipedia.org/wiki/Python_(programming_language)↩︎

  3. https://en.wikipedia.org/wiki/JavaScript↩︎

  4. https://en.wikipedia.org/wiki/Java_(programming_language)↩︎

  5. https://en.wikipedia.org/wiki/Rust_(programming_language)↩︎

  6. https://en.wikipedia.org/wiki/Go_(programming_language)↩︎

  7. https://en.wikipedia.org/wiki/Swift_(programming_language)↩︎

  8. https://en.wikipedia.org/wiki/Objective-C↩︎

  9. https://beej.us/guide/bgclr/↩︎

  10. https://en.wikipedia.org/wiki/ANSI_C↩︎

  11. https://en.wikipedia.org/wiki/POSIX↩︎

  12. https://visualstudio.microsoft.com/vs/community/↩︎

  13. https://docs.microsoft.com/en-us/windows/wsl/install-win10↩︎

  14. https://developer.apple.com/xcode/↩︎

  15. https://beej.us/guide/bgc/↩︎

  16. https://en.cppreference.com/↩︎

  17. https://groups.google.com/g/comp.lang.c↩︎

  18. https://www.reddit.com/r/C_Programming/↩︎

  19. https://en.wikipedia.org/wiki/Assembly_language↩︎

  20. https://en.wikipedia.org/wiki/Bare_machine↩︎

  21. https://en.wikipedia.org/wiki/Operating_system↩︎

  22. https://en.wikipedia.org/wiki/Embedded_system↩︎

  23. https://en.wikipedia.org/wiki/Rust_(programming_language)↩︎

  24. https://en.wikipedia.org/wiki/Grok↩︎

  25. 我知道有人会为这事跟我争,但它至少得排进前三吧?↩︎

  26. 嗯,严格来说不止两种,不过嘿,咱们就假装只有两种吧——无知是福,对吧?↩︎

  27. https://en.wikipedia.org/wiki/Assembly_language↩︎

  28. https://en.wikipedia.org/wiki/Machine_code↩︎

  29. 严格来说,它包含了一些预处理指令和函数原型(后面会细说),用于满足常见的输入输出需求。↩︎

  30. https://en.wikipedia.org/wiki/Unix↩︎

  31. 如果你不给它指定输出文件名,它默认会导出到名为 a.out 的文件——这个文件名深深扎根于 Unix 的历史。↩︎

  32. https://formulae.brew.sh/formula/gcc↩︎

  33. “字节”通常是一个 8 位二进制数。可以把它看成一个只能存放 0 到 255(含)这些值的整数。严格来说,C 允许字节是任意位数;如果你想明确指代一个 8 位数字,应该使用八位组(octet)这个说法。但程序员听到你说“字节”时,都会默认你指的是 8 位,除非你另有说明。↩︎

  34. 我在这里把现代内存的工作方式过度简化了。但这个心智模型是管用的,所以请见谅。↩︎

  35. 我在这里稍微撒了个谎。严格来说,3.14159 的类型是 double,但我们还没讲到那里,而且我希望你把 float 与“浮点数”联系起来;C 会很乐意把这种类型转换成 float。简而言之,在那之前不用管它。↩︎

  36. 把它读作“指向 char 的指针”或“char 指针”。“Char”即 character(字符)。虽然我找不到相关研究,但据传闻,多数人把它读作“char”,少数人读作“car”,还有极少数人读作“care”。关于指针我们以后再详谈。↩︎

  37. 口语中我们说它们具有“随机”值,但它们并不是真正的随机数——甚至连伪随机都算不上。↩︎

  38. 严格来说,这并非百分之百正确。等我们学到静态存储期时,你会发现有些变量会被自动初始化为零。不过稳妥的做法是始终初始化它们。↩︎

  39. 严格来说,bool 只用 char 的一个位来表示,因此它只能是 0 或 1。只不过 char 剩余(填充)位里放什么并未指明。对于 false,那些位必定全为零。但对于 true,我不确定是否必须全为零。↩︎

  40. _t 是 type 的简写。↩︎

  41. 变长数组除外——不过这又是另一个话题了。↩︎

  42. https://beej.us/guide/bgclr/html/split/stdlib.html#man-srand↩︎

  43. 这被认为是极大的隐患,以至于 Go 编程语言的设计者把 break 设为默认行为;如果你想落入下一个 case,必须显式使用 Go 的 fallthrough 语句。↩︎

  44. 通常如此。我相信在计算史幽暗的走廊中,总会存在例外。↩︎

  45. 一个字节是由不超过 8 个二进制位(简称 位)组成的数。这意味着,用奶奶那辈人惯用的十进制数字来说,它能存放 0 到 255(含两端)的无符号数。↩︎

  46. 字节的排列顺序被称为这个数的字节序。常见的是大端序(最高有效字节在前)和小端序(最高有效字节在后),如今不常见的则是混合端序(最高有效字节位于别处)。↩︎

  47. 也就是说,以 16 为基数,数字为 0、1、2、3、4、5、6、7、8、9、A、B、C、D、E 和 F。↩︎

  48. https://en.wikipedia.org/wiki/Virtual_memory↩︎

  49. 还不止这些!它还用于 /*comments*/、乘法,以及带变长数组的函数原型!全都是同一个 *,但上下文赋予了它不同的含义。↩︎

  50. https://en.wikipedia.org/wiki/Null_pointer#History↩︎

  51. https://en.wikipedia.org/wiki/Sentinel_value↩︎

  52. 指针类型的变量是 a、d、f 和 i,因为只有它们前面带 *。↩︎

  53. 反正如今是这样。↩︎

  54. 再说一遍,也不是真的;不过变长数组——我本人并不怎么喜欢——是另一个话题了。↩︎

  55. 因为在底层,数组不过是指向数组首元素的指针,所以并没有额外信息记录长度。↩︎

  56. 因为当你把数组传给函数时,实际传递的只是指向该数组首元素的指针,而不是“整个”数组。↩︎

  57. 在内存保护尚未出现的 MS-DOS 美好年代,我写过一些格外粗暴的 C 代码,故意制造各种未定义行为。但我清楚自己在做什么,而且一切运转得相当不错。直到我走错一步,导致系统卡死,重启后才发现,我的 BIOS 设置被全部抹掉了。真是有趣。(向 @man 致敬,感谢那段欢乐时光。)↩︎

  58. 导致未定义行为的原因很多,并不只有数组越界访问。这正是 C 语言如此令人兴奋的原因。↩︎

  59. https://en.wikipedia.org/wiki/Row-_and_column-major_order↩︎

  60. 严格来说这并不正确,因为指向数组的指针和指向数组首元素的指针类型不同。不过那座桥等我们走到了再烧也不迟。↩︎

  61. C11 §6.7.6.2¶1 要求它必须大于零。但你可能会看到有人在 struct 末尾声明零长度数组,而 GCC 对此格外宽容,除非你用 -pedantic 编译。这种零长度数组曾是一种取巧的机制,用来构造变长结构体。遗憾的是,访问这样的数组严格来说是未定义行为,尽管它基本上在任何地方都能用。C99 为它确立了一个明确定义的替代方案,叫做柔性数组成员,我们以后再聊。↩︎

  62. 下面这种写法也等价:void print_2D_array(int (*a)[3]),不过我现在还不想讲那么多。↩︎

  63. 不过 C 确实不记录字符串的长度,这一点倒是真的。↩︎

  64. 如果你用的是基本字符集或 8 位字符集,你会习惯一个字符就是一个字节。但并非所有字符编码都是如此。↩︎

  65. 它不同于 NULL 指针;谈到这个字符时我会把它缩写为 NUL,而指针则用 NULL。↩︎

  66. 稍后我们会学到用指针算术实现的更简洁的做法。↩︎

  67. 还有一个名为 strncpy() 的函数,它会限制复制的字节数。有人说因为有缓冲区溢出保护,你应该始终使用 strncpy()。也有人说你绝不该用 strncpy(),因为它不一定会为字符串补上终止符,这又是一处荒唐的自伤式设计。如果你真想安全,可以自己写一个始终补终止符的 strncpy() 版本。↩︎

  68. 虽然在 C 中,内存里的单个数据项(比如 int)被称为“对象”,但它们并不是面向对象编程意义上的对象。↩︎

  69. Saturn 曾是美国一个受欢迎的经济型汽车品牌,直到 2008 年金融危机让它关门大吉——对我们这些粉丝来说实在可惜。↩︎

  70. 在 64 位系统上,指针很可能是 8 字节。↩︎

  71. 深拷贝会顺着 struct 里的指针,把它们指向的数据也一并复制。浅拷贝只复制指针,而不复制它们指向的内容。C 不提供任何内置的深拷贝功能。↩︎

  72. https://beej.us/guide/bgclr/html/split/stringref.html#man-strcmp↩︎

  73. https://beej.us/guide/bgclr/html/split/stringref.html#man-memset↩︎

  74. https://stackoverflow.com/questions/141720/how-do-you-compare-structs-for-equality-in-c↩︎

  75. 过去广泛存在三种不同的换行符:回车(CR,用于旧版 Mac)、换行(LF,用于 Unix 系统)和回车/换行(CRLF,用于 Windows 系统)。所幸随着基于 Unix 的 OS X 问世,这个数字降到了两种。↩︎

  76. 如果缓冲区不够大,无法读入整行,它就会在行中途停止读取,下一次调用 fgets() 会继续读取该行剩下的部分。↩︎

  77. 通常第二个程序会一次性读入所有字节,然后再在循环中打印出来。那样效率更高。不过我们这里图的是演示效果。↩︎

  78. https://en.wikipedia.org/wiki/Hex_dump↩︎

  79. https://en.wikipedia.org/wiki/Endianess↩︎

  80. 这就是我上面在 fwrite() 和 fread() 的示例中明智地使用单个字节的原因。↩︎

  81. https://en.wikipedia.org/wiki/Protocol_buffers↩︎

  82. 这些我们以后再详细讨论。↩︎

  83. 回想一下,sizeof 运算符会告诉你内存中某个对象以字节计的大小。↩︎

  84. 或者字符串,它其实就是一个由 char 组成的数组。有点奇特的是,你还可以有一个指向数组末尾再往后一个位置的指针,这不会有问题,而且照样能在它上面做指针运算。只是当它指到那里时,你不能解引用它。↩︎

  85. https://beej.us/guide/bgclr/html/split/stdlib.html#man-qsort↩︎

  86. https://beej.us/guide/bgclr/html/split/stdlib.html#man-bsearch↩︎

  87. 因为要记住,数组记法不过是一次解引用再加上一些指针运算,而 void* 是不能解引用的!↩︎

  88. 你也可以把 void* 强制转换成另一种类型,不过我们还没讲到强制类型转换。↩︎

  89. 或者直到程序退出为止,那时它分配的所有内存都会被释放。补充一点:有些系统允许你分配在程序退出后仍然存在的内存,但这取决于系统,超出本指南的范围,而且你肯定不会无意中这么干。↩︎

  90. http://www.open-std.org/jtc1/sc22/wg14/www/docs/summary.htm#dr_460↩︎

  91. https://en.wikipedia.org/wiki/Bit_bucket↩︎

  92. “Bit” 是 binary digit(二进制位)的缩写。二进制只是表示数字的另一种方式。它用的不是我们习惯的 0 到 9 这十个数字,而是 0 和 1。↩︎

  93. https://en.wikipedia.org/wiki/Two%27s_complement↩︎

  94. 对一个恰好、确凿无疑地由 8 个位构成的序列,业界术语是八位组(octet)。↩︎

  95. 一般而言,如果你有一个 \(n\) 位的补码数,有符号范围是 \(-2^{n-1}\) 到 \(2^{n-1}-1\)。而无符号范围是 \(0\) 到 \(2^n-1\)。↩︎

  96. https://en.wikipedia.org/wiki/ASCII↩︎

  97. https://en.wikipedia.org/wiki/List_of_information_system_character_sets↩︎

  98. https://en.wikipedia.org/wiki/Unicode↩︎

  99. 这取决于 char 默认是 signed char 还是 unsigned char。↩︎

  100. https://en.wikipedia.org/wiki/Signed_number_representations#Signed_magnitude_representation↩︎

  101. 我的 char 是有符号的。↩︎

  102. https://en.wikipedia.org/wiki/IEEE_754↩︎

  103. 这个程序在 FLT_DIG 为 6、并使用 IEEE-754 二进制浮点数的系统上运行,结果会如它的注释所示。否则,你可能得到不同的输出。↩︎

  104. C 的规范里至今还没有这个,实在让我惊讶。在 C99 Rationale 文档中,他们写道:“一项添加二进制常量的提案被否决了,理由是缺乏先例、实用性不足。”考虑到他们往规范里一股脑塞进去的其他一些特性,这话显得有点可笑!我敢打赌下一个发布的版本就会有的。↩︎

  105. https://en.wikipedia.org/wiki/Scientific_notation↩︎

  106. 两者相同,只不过 snprintf() 允许你指定输出的最大字节数,从而防止写越字符串末尾。所以它更安全。↩︎

  107. https://en.wikipedia.org/wiki/ASCII↩︎

  108. 我们必须把指向 badchar 的指针传给 strtoul(),否则它无论怎么修改,我们都看不到;这类似于:如果你想让某个函数能改变某个 int 的值,就必须把指向该 int 的指针传给这个函数。↩︎

  109. 在任意给定的字符编码方案中,每个字符都有一个与之关联的值。↩︎

  110. 在实践中,你的实现上很可能发生的是:结果中的高位被直接丢弃,所以一个 16 位数 0x1234 转换成 8 位数后,结果是 0x0034,也就是 0x34。↩︎

  111. 同样,实践中你的系统上很可能发生的是:原值的位模式被截断,然后直接用来表示有符号数,即补码。例如,我的系统把值为 192 的 unsigned char 转换成 signed char 的 -64。在补码中,这两个数的位模式都是二进制 11000000。↩︎

  112. 其实不会——它只是被照常丢弃了。↩︎

  113. 即可变参数函数。↩︎

  114. 这种做法很少见,因为编译器会抱怨,而且有函数原型才是正确的做法。我认为它至今仍然可行是出于历史原因——那时函数原型这回事还不存在。↩︎

  115. https://beej.us/guide/bgclr/html/split/ctype.html↩︎

  116. https://gustedt.wordpress.com/2010/08/17/a-common-misconsception-the-register-keyword/↩︎

  117. https://en.wikipedia.org/wiki/Processor_register↩︎

  118. https://en.wikipedia.org/wiki/Boids↩︎

  119. 历史上,MS-DOS 和 Windows 程序在这件事上的做法与 Unix 不同。在 Unix 中,shell 会在你的程序看到通配符之前,先把它展开成所有匹配的文件;而微软的各个变体则把通配符表达式传进程序,让程序自己去处理。不管怎样,总有一些参数会被传进程序。↩︎

  120. 由于它们只是普通的形参名,你其实不必把它们叫作 argc 和 argv。但用这些名字实在太符合习惯了,要是你发挥创意,其他 C 程序员真会用怀疑的眼光看你!↩︎

  121. ps,即 Process Status,是一条 Unix 命令,用来查看当前有哪些进程在运行。↩︎

  122. https://en.wikipedia.org/wiki/Inception↩︎

  123. https://en.wikipedia.org/wiki/Shell_(computing)↩︎

  124. 在 Windows 的 cmd.exe 中,输入 echo %errorlevel%。在 PowerShell 中,输入 $LastExitCode。↩︎

  125. 如果你需要一个数值,可以用 atoi() 或 strtol() 之类的东西把字符串转换过去。↩︎

  126. 在 Windows CMD.EXE 中,使用 set FROTZ=value。在 PowerShell 中,使用 $Env:FROTZ=value。↩︎

  127. https://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html↩︎

  128. 你不能总是简单地把代码用 /* */ 注释包起来,因为这种注释不能嵌套。↩︎

  129. 这其实不算宏——严格说来它是一个标识符。但它是唯一预定义的标识符,而且感觉很像宏,所以我把它放在这里。像个叛逆者。↩︎

  130. 托管实现基本上意味着你在运行完整的 C 标准,很可能是在某种操作系统上。你多半就是这种情况。如果你是在某种嵌入式系统的裸机上运行,那你多半是在独立实现上。↩︎

  131. 好吧,我知道那是个敷衍的答案。基本上,编译器可以实现一个可选的扩展,承诺限制某些类型的未定义行为,从而让 C 代码更适合做静态代码分析。你不太可能需要用到它。↩︎

  132. 破坏规矩……破坏规矩……↩︎

  133. https://www.openmp.org/↩︎

  134. 严格说来,我们说它有一个不完整类型。↩︎

  135. 不过有些编译器提供了强制产生这种效果的选项——搜索 __attribute__((packed)) 就能看到在 GCC 里怎么做。↩︎

  136. 顺便说一句,super 并不是关键字。我只是借用了些面向对象(OOP)的术语。↩︎

  137. 假设 char 是 8 位的,即 CHAR_BIT == 8。↩︎

  138. https://en.wikipedia.org/wiki/Type_punning↩︎

  139. 那个数字是我随口编的,不过大概也差不了太远。↩︎

  140. 对于只存放在寄存器里的值,细节上还有些麻烦,但就我们这里的目的而言可以放心忽略。另外,除了 register 关键字之外,C 规范对这些“寄存器”玩意儿并没有表态,而该关键字的说明里根本没提到寄存器。↩︎

  141. 你自己的机器上很可能得到不同的数字。↩︎

  142. 规范里绝对没有任何内容说它总是这样工作,但在我的系统上恰好就是如此。↩︎

  143. 奇怪的是,事实证明即使 E 是 NULL 也照样如此。↩︎

  144. https://beej.us/guide/bgclr/html/split/stringref.html#man-memcpy↩︎

  145. C 编译器没有义务添加填充字节,而且即便添加了,这些填充字节的取值也是不确定的。↩︎

  146. 这会因架构而异,但我的系统是小端序,也就是说数字的最低有效字节先被存储。大端序系统则会先存 12、最后存 78。不过规范对这种表示方式没有任何规定。↩︎

  147. 这是可选特性,所以它可能不存在——但多半是存在的。↩︎

  148. 我是倒着打印这些 16 位值的,因为我的机器是小端序,这样在这里读起来更方便。↩︎

  149. 假设它们指向同一个数组对象。↩︎

  150. 《Go 程序设计语言》的类型声明语法,灵感正是来自 C 做法的反面。↩︎

  151. 这并不是说其他语言不这么做——它们也这么做。有趣的是,许多现代语言在按位运算上用的运算符和 C 一样。↩︎

  152. https://en.wikipedia.org/wiki/Bitwise_operation↩︎

  153. 也就是说,我们这些卑微的开发者本就不该知道里面装的是什么、又代表什么。规范并没有详细规定它究竟是什么。↩︎

  154. 老实说,从语言里移除这个限制是有可能的,但设计意图是:宏 va_start()、va_arg() 和 va_end() 应当能够用 C 写出来。要做到这一点,我们需要某种办法把指针初始化到第一个形参所在的位置。而要这么做,我们就需要第一个形参的名字。这需要扩展语言才能实现,而到目前为止,委员会还没找到这么做的理由。↩︎

  155. “这颗星球有一个问题——或者说曾有过——问题就在于:生活在其上的大多数人,绝大多数时候都不快乐。人们为这个问题提出过许多解决方案,但其中大部分主要关心的是那些绿色小纸片的流动,这很奇怪,因为总的来说,不快乐的并不是那些绿色小纸片。”——《银河系漫游指南》,道格拉斯·亚当斯↩︎

  156. 记住,char 不过就是一个字节大小的整数。↩︎

  157. 不过 isdigit() 和 isxdigit() 除外。↩︎

  158. 例如,我们可以把码点存进一个大端序的 32 位整数。直截了当!我们刚发明了一种编码!其实并没有;那正是 UTF-32BE 编码。好吧——回去继续埋头苦干吧!↩︎

  159. 算是吧。严格说来,它的宽度是可变的——可以把两个 UTF-16 字符拼在一起,来表示高于 \(2^{16}\) 的码点。↩︎

  160. 有一个特殊字符叫作字节顺序标记(BOM),码点为 0xFEFF,可以选择性地放在数据流前面,用来指示字节序。不过它并不是必需的。↩︎

  161. 再说一次,这只在 UTF-16 中、且只对能放进两个字节的字符成立。↩︎

  162. https://en.wikipedia.org/wiki/UTF-8↩︎

  163. https://www.youtube.com/watch?v=MijmeoH9LT4↩︎

  164. 编译器大概会尽力把码点转换成输出编码所对应的东西,但我在规范里找不到任何保证。↩︎

  165. 比如用 "%.12s" 这样的格式说明符。↩︎

  166. wcscoll() 等价于先调用 wcsxfrm() 再调用 wcscmp()。↩︎

  167. 算是吧——碰上跨多个 char16_t 的 UTF-16 编码,情况就变得古怪了。↩︎

  168. https://en.wikipedia.org/wiki/Iconv↩︎

  169. http://site.icu-project.org/↩︎

  170. https://en.wikipedia.org/wiki/Core_dump↩︎

  171. 看起来它在底层根本不支持 Unix 风格的信号,控制台程序里的信号是模拟出来的。↩︎

  172. 令人困惑的是,sig_atomic_t 比无锁原子类型出现得更早,两者并不是一回事。↩︎

  173. 如果 sig_action_t 是有符号的,范围至少是 -127 到 127。如果是无符号的,至少是 0 到 255。↩︎

  174. 这是因为变长数组通常分配在栈上,而 static 变量位于堆上。变长数组的整个思路就是:函数结束时栈帧弹出,它们会被自动释放。↩︎

  175. https://en.wikipedia.org/wiki/Goto#Criticism↩︎

  176. 我想指出,在所有这些情形里使用 goto 都是可以避免的。你可以改用变量和循环。只不过有些人认为在那些情况下 goto 能生成最好的代码。↩︎

  177. https://en.wikipedia.org/wiki/Tail_call↩︎

  178. 这并不完全相同,因为它是数组,而不是指向 int 的指针。↩︎

  179. 这里用到的变量确实是一个表达式。↩︎

  180. “栈指针”和“程序计数器”都与底层架构和 C 实现有关,并不属于规范的一部分。↩︎

  181. 这里的理由是,程序在处理某个值时,可能会把它临时存放在 CPU 寄存器中。在这段时间里,寄存器里保存的是正确的值,而栈上的值可能已经过时。之后寄存器的值会被覆盖,对变量所做的改动也就丢失了。↩︎

  182. 也就是说,它们会一直保持已分配状态直到程序结束,而且没有办法释放。↩︎

  183. 这之所以可行,是因为在 C 中,无论指针指向何种类型的数据,指针的大小都相同。所以编译器此时不需要知道 struct node 的大小,只需要知道一个指针有多大。↩︎

  184. https://en.wikipedia.org/wiki/Complex_number↩︎

  185. 这一条查起来更费劲,谁要是能提供更多信息,我都乐意接受。I 可以被定义为 _Complex_I 或 _Imaginary_I(如果后者存在的话)。_Imaginary_I 会处理有符号零,而 _Complex_I 可能不会。这会牵扯到分支割线以及其他与复数相关的数学玩意儿。也许吧。你能看出来我在这儿确实有点力不从心吗?无论如何,CMPLX() 这些宏的行为都好像 I 被定义为带符号零的 _Imaginary_I,即使系统上并不存在 _Imaginary_I。↩︎

  186. 这句话看似简单,却远不能体现为了搞懂浮点数究竟如何运作所需付出的惊人努力。https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/↩︎

  187. 奇怪的是,它是唯一一个前面不多带一个 c 的。↩︎

  188. 有些架构上,CPU 和 RAM 处理某些宽度的数据比其他宽度更快。在这种情况下,如果你想要最快的 8 位整数,它反而可能给你一个 16 位或 32 位的类型,因为那样就是更快。所以用了它,你不会知道这个类型到底多大,但它至少有你所说的那么大。↩︎

  189. 也就是说,系统拥有 8、16、32 或 64 位、无填充、采用补码表示的整数,在这种情况下,对应特定位数的 intN_t 变体必须被定义。↩︎

  190. 反正地球上如此。谁知道外面用的又是什么疯狂的系统……↩︎

  191. 好吧,别杀我!严格来说,GMT 是一个时区,而 UTC 是一套全球时间系统。另外,有些国家可能会对 GMT 做夏令时调整,而 UTC 从不做夏令时调整。↩︎

  192. 得承认,其实不止两个。↩︎

  193. https://en.wikipedia.org/wiki/Unix_time↩︎

  194. https://beej.us/guide/bgclr/html/split/time.html#man-strftime↩︎

  195. 在 POSIX 上你就会知道,那里 time_t 绝对是整数。不幸的是,全世界并不都是 POSIX,所以只能这样了。↩︎

  196. https://en.wikipedia.org/wiki/POSIX_Threads↩︎

  197. 我个人更喜欢“无共享”的路子,而且至少可以说,我对经典多线程构造的运用已经生疏了。↩︎

  198. 没错,是 pthreads,带一个“p”。它是 POSIX threads 的缩写,C11 的线程实现大量借鉴了这个库。↩︎

  199. 见 §7.1.4¶5。↩︎

  200. 除非你调用 thrd_detach()。后面还会细说。↩︎

  201. 不过我并不认为它们非得如此。只是这些线程似乎要等到发生某种系统调用(比如 printf() 可能引发的那种)才会被重新调度……这也是我在里面放了 printf() 的原因。↩︎

  202. 是“互斥”(mutual exclusion)的缩写,也就是给一段代码加一把“锁”,只允许一个线程执行。↩︎

  203. 也就是说,你的进程会进入睡眠。↩︎

  204. 你可能以为它是“从现在开始计时”,不过那只是你的一厢情愿,不是吗!↩︎

  205. 这就是它们被称为条件变量的原因!↩︎

  206. 我不是说这是外星人干的……但就是外星人。好吧,更可能的情况是另一个线程先被唤醒,抢先干了这活。↩︎

  207. 适者生存!对吧?我承认实际情况根本不是这样。↩︎

  208. 早期的 C89 里没有 __STDC_VERSION__ 宏,所以如果你担心这一点,就用 #ifdef 检查一下。↩︎

  209. 原因是,优化之后,我的编译器把 x 的值放进了寄存器,好让 while 循环跑得快些。但寄存器无从得知这个变量在另一个线程里被更新过,所以它永远看不到 3490。这其实和原子性中全有或全无的那部分关系不大,而更多关系到下一节要讲的同步方面。↩︎

  210. 除非我另有说明,否则我讲的通常都是顺序一致的操作。这意味着什么,很快会讲到。↩︎

  211. 从程序员的角度看,这是最理智的做法。↩︎

  212. 据说 C++23 会把它加成一个宏。↩︎

  213. 规范指出,它们在大小、表示和对齐上可能有所不同。↩︎

  214. 那个例子是我凭空捏造的。也许在 Intel/AMD 上无所谓,但在别的地方可能就有所谓,可恶!↩︎

  215. C++ 进一步说明:如果信号是由调用 raise() 产生的,那么它排在 raise() 之后。↩︎

  216. https://en.wikipedia.org/wiki/Test-and-set↩︎

  217. 因为 consume 关注的完全是那些依赖于所获取的原子变量之值的操作,而栅栏并没有对应的原子变量。↩︎

  218. https://www.youtube.com/watch?v=A8eCGOqgvH4↩︎

  219. https://www.youtube.com/watch?v=KeLBd2EJLOU↩︎

  220. https://preshing.com/archives/↩︎

  221. https://preshing.com/20120612/an-introduction-to-lock-free-programming/↩︎

  222. https://preshing.com/20120913/acquire-and-release-semantics/↩︎

  223. https://preshing.com/20130702/the-happens-before-relation/↩︎

  224. https://preshing.com/20130823/the-synchronizes-with-relation/↩︎

  225. https://preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/↩︎

  226. https://preshing.com/20150402/you-can-do-any-kind-of-atomic-read-modify-write-operation/↩︎

  227. https://en.cppreference.com/w/c/atomic/memory_order↩︎

  228. https://en.cppreference.com/w/c/language/atomic↩︎

  229. https://docs.microsoft.com/en-us/windows/win32/dxtecharts/lockless-programming↩︎

  230. https://www.reddit.com/r/C_Programming/↩︎

  231. 除非你开着优化编译(多半如此)!不过我认为它这么做时并不符合规范。↩︎

  232. https://beej.us/guide/bgclr/html/split/stdlib.html#man-exit↩︎

  233. https://beej.us/guide/bgclr/html/split/stdlib.html#man-abort↩︎

  234. https://en.wikipedia.org/wiki/Data_structure_alignment↩︎


| 目录 |