要点汇总
本页汇集全书各章的要点,可作速查表使用。点击要点编号,可跳回正文中该要点所在的小节。
前言
C 与 C++ 是不同的语言:不要混用,也不要混淆。
第 1 章:入门
不要恐慌。(译者注:原文:“Don't panic.”,出自《银河系漫游指南》)
C 是一种命令式编程语言。
C 是一种编译型编程语言。
正确的 C 程序可以在不同平台之间移植。
C 程序应当干净利落地完成编译,不产生警告。
第 2 章:程序的基本结构
同一个标点字符可以用于多种不同目的。
程序中的所有标识符都必须声明。
一个标识符可以有多个相互一致的声明。
声明仅在其出现的作用域内有效。
声明说明标识符,定义说明对象。
对象在初始化的同时得到定义。
初始化器中缺少的元素默认为 0。(译者注:此处的“0”应当理解为与该元素类型相对应的“零值(Zero Value)”。整数为 0,浮点数为 0.0,指针为 NULL,等等。)
对于含有 n 个元素的数组,第一个元素的索引是 0,最后一个元素的索引是 n-1。
每个对象或函数都必须恰好有一个定义。
范围迭代应当用 for 语句编写。
循环对象应当定义在 for 的起始部分中。
第 3 章:一切都与控制有关
值 0 表示逻辑假。
任何不等于 0 的值都表示逻辑真。
不要与 0、false 或 true 作比较。
所有标量都有真值。
case 值必须是整数常量表达式。
每个 switch 语句中的 case 值必须唯一。
case 标签不得跳过对象定义。
第 4 章:表达计算
size_t 类型表示范围 [0, SIZE_MAX] 内的值。
无符号算术始终是良好定义的。
如果数学上的正确结果可以表示为 size_t,那么 size_t 上的 +、- 和 * 运算就会给出该结果。
对于无符号值,a == (a/b) * b + (a%b)。
只有第二个操作数不为 0 时,无符号 / 和 % 才是良好定义的。
size_t 算术会隐式地以 SIZE_MAX + 1 为模进行计算。
发生溢出时,无符号算术会回绕。
无符号 / 和 % 的结果始终不大于操作数。
无符号 / 和 % 不会溢出。
一个运算符的所有字符必须彼此直接相连。
值表达式中的副作用是邪恶的。
绝不要在一个语句中修改一个以上的对象。
比较运算符返回值 false 或 true。
逻辑运算符返回值 false 或 true。
&&、||、?: 和 , 首先对第一个操作数求值。
不要使用 , 运算符。
大多数运算符不会为其操作数定序。
函数调用不会为其实参表达式定序。
表达式中的函数调用不应带有副作用。
第 5 章:基本值与数据
C 程序首先围绕值推理,而不是围绕值的表示推理。
所有值都是数,或者能够转换为数。
所有值都有一个静态确定的类型。
可对一个值执行哪些操作,由它的类型决定。
一个值的类型决定所有操作的结果。
一个类型的二进制表示决定所有操作的结果。
一个类型的二进制表示是可观察的。
程序的执行,如同遵循抽象状态机。
类型决定优化机会。
执行算术之前,窄整数会提升为 signed int。
四类基本类型各有三种互不相同、无需提升的类型。
大小、基数或序数应使用 size_t。
对不可能为负的小数量使用 unsigned。
对带符号的小数量使用 signed。
对带符号的大差值使用 ptrdiff_t。
浮点计算使用 double。
复数计算使用 double complex。
相邻的字符串字面量会连接在一起。
数值字面量从不为负。
十进制整数字面量是有符号的。
十进制整数字面量采用三种有符号类型中第一个能够容纳其值的类型。
同一个值可以具有不同类型。
不要用二进制、八进制或十六进制字面量表示负值。
负值应使用十进制字面量。
不同字面量可以具有相同的值。
十进制浮点字面量的实际值可能不同于其字面值。
字面量具有值、类型和二进制表示。
I 保留用于虚数单位。
一元 - 和 + 的类型,就是其提升后操作数的类型。
避免窄化转换。
不要用窄类型执行算术。
避免对符号性不同的操作数执行操作。
只要能够做到,就使用无符号类型。
选择算术类型时,应确保隐式转换无害。
所有对象都应初始化。
所有聚合数据类型都应使用指派初始化器。
{ } 对所有对象都是有效的初始化器。
具有特定含义的所有常量都必须命名。
含义不同的所有常量都必须彼此区分。
具有 const 限定类型的对象是只读的。
字符串字面量是只读的。
枚举常量要么具有显式指定的值,要么具有按位置确定的值。
如果简单枚举类型的所有枚举常量都能装入 signed int,这些常量就具有该类型。
整数常量表达式只能对声明为 constexpr 的对象求值。
如果枚举常量无法装入 signed int,那么只要可能,枚举类型就会调整为能够存放所有枚举常量的类型。
如果枚举常量无法装入 signed int,这些常量就采用枚举类型。
如果枚举常量有可能无法全部装入 signed int,就应指定枚举类型的底层整数类型。
宏名全部使用大写字母。
复合字面量定义一个对象。
不要把结尾分号藏进宏里。
把宏的续行标记右对齐到同一列。
constexpr 的初始化器必须精确适配。
同一个值可以具有不同的二进制表示。
任何整数类型的最大值都具有 2ᵖ − 1 的形式。
无符号整数类型上的算术由其精度决定。
移位操作的第二个操作数必须小于精度。
正值的表示与符号性无关。
抽象状态机一旦到达未定义状态,就不能再对后续执行作出任何假设。
避免所有操作产生未定义行为,是你的责任。
如果程序状态到达具有未定义行为的操作,执行就已经失效。
有符号算术可能以恶劣方式触发陷阱。
INT_MIN < -INT_MAX。
有符号算术中的取负可能溢出。
位操作应使用无符号类型。
如果提供了类型 uintN_t,它就是宽度和精度都恰为 N 位的无符号整数类型。
如果提供了类型 intN_t,它就是宽度恰为 N 位、精度为 N − 1 的有符号整数类型。
对于值 N,如果存在具备所需性质的类型,就必须提供 intN_t 和 uintN_t。
对所提供的任意固定宽度类型,还会提供宽度 _WIDTH、最小值 _MIN(仅有符号类型)、最大值 _MAX 和字面量 _C 宏。
浮点操作既不满足结合律,也不满足交换律和分配律。
绝不要比较浮点值是否相等。
第 6 章:派生数据类型
数组不是指针。
数组用于条件时,求值结果为 true。
存在数组对象,但不存在数组值。
数组不能比较。
不能向数组赋值。
VLA 只能使用默认初始化器。
不能在函数外声明 VLA。
FLA 的长度由整数常量表达式或初始化器确定。
数组长度说明必须严格为正。
如果长度不是整数常量表达式,数组就是 VLA。
数组 A 的长度是 (sizeof A)/(sizeof A[0])。
函数的数组形参会丢失最内层维度。
不要对函数的数组形参使用 sizeof 运算符。
数组形参表现得如同数组按引用传递。
字符串是以 0 结尾的 char 数组。
把非字符串传给字符串函数会导致程序失效。
指针是不透明对象。
指针可能有效、为空,也可能无效。
用 nullptr 初始化指针或给指针赋值,会使指针为空。
在逻辑表达式中,空指针求值为 false。
无效指针会导致程序失效。
始终初始化指针。
结构体初始化器中省略的成员会被强制初始化为 0。
struct 形参按值传递。
结构体可以赋值。
结构体不能使用 == 或 != 比较。
结构体布局是一项重要的设计决策。
嵌套声明中的所有 struct 声明具有相同的可见作用域。
任意结构体成员之后都可能存在填充。
结构体开头没有填充。
指定一个位域时,不要单独使用 int。
宽度为 N 的数值位域应使用 _BitInt(N) 类型。
宽度为 1 的标志位域应使用 bool 类型。
在 typedef 中前置声明 struct 时,应使用与标记名相同的标识符。
typedef 只为类型创建别名,绝不会创建新类型。
以 _t 结尾的标识符名称是保留名称。
第 7 章:函数
所有函数都必须有原型。
函数只有一个入口,却可以有多个返回点。
函数的返回必须与其类型一致。
到达函数体末尾,等价于执行一条不带表达式的 return 语句。
只有 void 函数才允许执行到函数体末尾。
把 EXIT_SUCCESS 和 EXIT_FAILURE 用作 main 的返回值。
执行到 main 末尾,等价于以 EXIT_SUCCESS 执行 return。
调用 exit(s),等价于在 main 中对 return s 求值。
exit 从不失败,也绝不会返回调用方。
所有命令行实参都以字符串形式传递。
argv[0] 指向程序调用时所用的名称。
argv[argc] 是空指针。
明确写出函数的所有前置条件。
在递归函数中,首先检查终止条件。
在包装函数中保证递归函数的前置条件。
多重递归可能导致执行时间呈指数增长。
糟糕的算法绝不会带来高性能实现。
改进算法可以显著提升性能。
第 8 章:C 库函数
失败永远是一种可能。
检查库函数的返回值是否表示错误。
快速失败,尽早失败,经常失败。
以下划线后接 s(_s)结尾的标识符名称是保留的。
如果执行平台不满足前置条件,必须中止编译。
在预处理条件指令中,只求取宏和整数字面量。
在预处理条件指令中,未知标识符求值为 0。
不透明类型通过函数接口来规定。
不要依赖不透明类型的实现细节。
puts 与 fputs 对行结束的处理不同。
文本输入和输出会转换数据。
常用的行结束编码转换共有三种。
文本行不应包含行尾空白。
printf 调用中的尾随实参必须与格式说明符严格对应。
使用 "%d" 和 "%u" 格式打印整数值。
使用 "%b" 或 "%x" 格式打印位模式。
使用 "%g" 格式打印浮点值。
使用不恰当的格式说明符或修饰符,会使行为未定义。
对稍后还要读回的转换,使用 "%+d"、"%#X" 和 "%a"。
不要使用 gets。
fgetc 返回 int,是为了在所有有效字符之外还能编码特殊错误状态 EOF。
只有读取失败后才能检测到文件结束。
数值编码字符的解释取决于执行字符集。
不要用字符串转换函数确定数值的边界。
不要用字符串转换函数扫描源自数值字面量的数。
正常终止程序应当从 main 返回。
在可能终止正常控制流的函数中使用 exit。
除非必须阻止库清理工作执行,否则不要使用 exit 以外的函数终止程序。
尽可能多用断言确认运行时性质。
在生产编译中,使用 NDEBUG 关闭所有断言。
第 9 章:风格
所有 C 代码都必须可读。
短期记忆容量很小,视野范围也很小。
编码风格不是品味问题,而是文化问题。
每个已经建立的项目都构成自己的文化空间。
为空白及其他文本格式选择一套一致的策略。
让文本编辑器自动把代码格式化正确。
为所有标识符选择一致的命名策略。
头文件中可见的任何标识符都必须符合标准。
不要污染全局标识符空间。
名称必须容易辨认,而且能够迅速区分。
命名是一种创造活动。
文件作用域标识符必须表意完整。
类型名标识一个概念。
全局常量标识一个事物。
全局对象标识状态。
函数或类函数宏标识一个动作。
选择项目的自然语言时,应以适合大多数参与者为准。
只有在 C 规范化形式下映射到自身的字母,才允许用于标识符。
只有直接源于自然语言,或者与所有自然语言字符都明显不同的字母,才可用于标识符。
只有当不同文字的字母或十进制数字的不同形式彼此明显可辨时,才在标识符中使用它们。
在标识符中使用下标或上标字母不可移植。
第 10 章:组织与文档
函数接口描述要做什么。
接口注释记录函数的用途。
函数代码展示函数如何组织。
代码注释解释函数细节以何种方式实现。
分离接口与实现。
为接口编写文档;对实现作出解释。
详尽记录接口。
把代码组织成语义联系紧密的单元。
按字面直接实现。
控制流必须一目了然。
宏不应以出人意料的方式改变控制流。
类函数宏在语法上应当表现得像函数调用。
函数形参按值传递。
全局对象不受欢迎。
只要可能,就把小任务表述为纯函数。
属性中的标识符可能被预处理替换。
在头文件中使用属性的双下划线形式。
第 11 章:指针
若程序执行把 * 用于无效指针或空指针,则执行失败。
有效指针引用一个由所引用类型构成的数组的首元素。
无法从指针还原数组对象的长度。
指针不是数组。
只能对指向同一数组对象元素的指针作减法。
所有指针差都具有 ptrdiff_t 类型。
使用 ptrdiff_t 编码位置差或大小差的有符号值。
打印指针值时,把它强制转换为 void*,并使用格式说明 %p。
指针具有真值。
尽早把指针对象设为空。
如果程序执行访问了一个对象,而该对象具有其类型的非值表示,则执行失败。
解引用时,指针所指对象必须具有指针指定的类型。
指针必须指向有效对象、刚好越过该对象一个位置,或者为空。
如果程序执行计算出的指针值超出数组对象边界(也不是刚好越过一个元素),则执行失败。
使用 nullptr,不要使用 NULL。
不要在 typedef 中隐藏指针类型。
表达式 A[i] 与 *(A + i) 等价。
对数组 A 求值会得到 &A[0]。
在函数声明中,任何数组形参都会改写成指针。
只有数组形参的最内层维度会被改写。
长度形参应当声明在数组形参之前。
函数数组实参的有效性必须由程序员保证。
不带后续圆括号的函数名,会退化成指向函数入口的指针。
函数指针必须采用完全匹配的类型。
函数调用运算符 (...) 作用于函数指针。
第 12 章:C 内存模型
基础类型不同的指针类型彼此不同。
按照定义,sizeof(char) 等于 1。
任何对象 A 都可以视为 unsigned char[sizeof A]。
指向字符类型的指针具有特殊地位。
字符和字符串数据使用 char 类型。
所有对象类型都以 unsigned char 类型作为基本单元。
sizeof 运算符可以用于对象和对象类型。
类型 T 的所有对象都具有大小 sizeof(T)。
算术类型各个表示数字在内存中的顺序由实现定义。
在大多数体系结构上,CHAR_BIT 等于 8,UCHAR_MAX 等于 255。
除字符类型外,只有基础类型相同的指针才能形成别名。
避免使用 & 运算符。
任何对象指针都能与 void* 相互转换。
对象具有存储、类型和值。
把对象指针转换成 void*,再转换回原来的类型,是恒等操作。
避免使用 void*。
不要使用强制转换。
必须通过对象的有效类型,或者通过指向字符类型的指针访问对象。
只要相应字节表示构成访问类型的有效值,具有有效联合体类型的对象就可以随时访问任何成员。
具名对象或复合字面量的有效类型,就是其声明中的类型。
具名对象与复合字面量必须通过声明类型,或者通过指向字符类型的指针访问。
第 13 章:存储
调用分配函数时,只能请求严格大于 0 的大小。
分配失败会产生空指针。
优先使用 strndup,而不是 strdup。
不要强制转换 malloc 及其同伴的返回值。
通过 malloc 分配的存储没有初始化,也没有类型。
malloc 通过返回空指针值表示失败。
每次分配都必须对应一次释放。
每次调用 free,都必须对应一次 malloc、calloc、aligned_alloc 或 realloc。
只能把 malloc、calloc、aligned_alloc 或 realloc 原样返回的指针传给 free。
带有柔性数组成员的结构体对象,必须拥有足以访问整个结构体的存储。
长度成员与柔性数组成员之间的一致性必须手工维护。
标识符只在其作用域内可见,而且可见性从声明处开始。
标识符的可见性可能被从属作用域中的同名标识符遮蔽。
具名对象的每一项定义都会创建一个彼此不同的新对象。
只读对象字面量可以相互重叠。
对象具有生存期;在生存期之外不能访问该对象。
如果程序执行在对象生存期之外引用该对象,则执行失败。
复合字面量的生存期,与在同一语境中按相同存储类声明的具名对象相同。
具有静态存储期的对象始终会初始化。
除 VLA 或临时对象外,自动对象的生存期与定义所在块的执行相对应。
每次递归调用都会为自动对象创建一个新的局部实例。
& 运算符不能用于以 register 声明的对象。
以 register 声明的对象不能形成别名。
在性能关键代码中,把非数组的局部对象声明为 register。
存储类为 register 的数组没有用处。
具有临时生存期的对象只读。
临时生存期终于外围完整表达式结束之时。
对于非 VLA 对象,生存期始于进入定义所在作用域,终于离开该作用域。
每次执行遇到定义时,都会对自动对象和复合字面量的初始化器求值。
VLA 的生存期始于执行遇到定义,终于离开可见作用域。
具有静态或线程存储期的对象会默认初始化。
具有自动或分配存储期的对象必须显式初始化。
系统地为每一种数据类型提供初始化函数。
第 14 章:进阶处理与输入输出
字符串 strto... 转换函数不具备 const 安全性。
memchr 和 strchr 搜索函数的函数接口不具备 const 安全性。
memchr 和 strchr 搜索函数的类型泛型接口具备 const 安全性。
strspn 和 strcspn 搜索函数具备 const 安全性。
sprintf 没有采取任何防止缓冲区溢出的措施。
格式化长度未知的输出时,请使用 snprintf。
多字节字符不包含空字节。
多字节字符串以空字符终止。
一个码点的多字节 mb 编码会一次性写入输出字符串。
一个码点的多字节 mb 编码可以从输入中分段收集。
凡是使用 fread 或 fwrite 的流,都应当以二进制模式打开。
以二进制模式写出的文件不能跨平台移植。
fseek 和 ftell 不适合非常大的文件偏移量。
第 15 章:程序故障
程序执行只应完成在底层类型范围内具有数学定义的算术操作。
平台的浮点环境决定哪些浮点操作会造成程序失效。
指针操作应当始终停留在数组对象边界以内。
只要可能,就使用数组索引,不要组合指针算术与解引用。
除非不可避免,否则不要转换指针。
始终按照函数定义所用的原型调用函数。
通过函数名称调用函数。
不要在 bool 对象中存储 0 和 1 以外的值。
不要直接改变对象的表示字节。
只有掌握证明时才能使用 unreachable()。
只能用 unreachable() 标记绝不会采用的控制路径,不要使用其他操作代替。
不要在同一个算术表达式中读取并修改同一个对象。
如果程序执行在有限状态集合上循环,而且没有可观察副作用,它就已经失效。
保证可能失效的操作满足全部前置条件。
可能耗尽资源的操作,应当检查其返回值是否表示错误。
只有谨慎设计算法,才能避免不幸事件。
goto 的标签在包含它的整个函数内可见。
goto 只能跳到同一个函数内的标签。
goto 不应跳过对象初始化。
第 16 章:性能
过早优化是万恶之源。
不要以安全性换取性能。
优化器足够聪明,能够消除未使用的初始化。
函数指针实参的不同记法会产生相同的二进制代码。
不取得局部对象的地址会抑制别名,因而有助于优化器。
内联可以开启大量优化机会。
增加一个不带 inline 关键字的兼容声明,可以确保在当前 TU 中生成函数符号。
内联函数定义在所有 TU 中可见。
内联定义应放在头文件中。
不带 inline 的附加声明应恰好放在一个 TU 中。
只有当你认为函数已经稳定时,才把它公开为 inline。
内联函数的所有局部标识符都应通过方便的命名约定加以保护。
inline 函数不能按名称访问 static 函数。
inline 函数不能按名称访问可修改的 static 对象。
inline 函数不能定义可修改的 static 对象。
由 restrict 限定的指针必须提供独占访问。
restrict 限定会约束函数调用方。
所有纯函数都应具有属性 [[unsequenced]]。
具有属性 [[unsequenced]] 的函数不得读取非常量全局对象或系统状态。
一般而言,使用浮点算术的函数不是纯函数,不得具有属性 [[unsequenced]]。
具有属性 [[unsequenced]] 的函数不得对全局对象或系统状态施加可见修改。
通过 errno 返回潜在错误的函数不是纯函数,不得具有属性 [[unsequenced]]。
改变浮点状态的编译指示,只在当前作用域内局部生效。
类型属性会在当前作用域内累积。
具有属性 [[reproducible]] 的函数可以临时修改全局状态,只要随后将其恢复为原值。
对具有 [[unsequenced]] 或 [[reproducible]] 属性的函数,应以 restrict 注解其指针形参。
具有属性 [[unsequenced]] 的函数不得修改局部静态状态,即使通过其他函数调用也不行。
具有属性 [[reproducible]] 的函数只能在局部静态状态无法从函数外部观察时修改该状态。
不要臆测代码性能;应当严谨验证。
评估算法复杂度需要证明。
评估代码性能需要测量。
所有测量都会引入偏差。
插桩会改变编译期性质与运行时性质。
运行时间的相对标准差必须处于较低百分比范围。
收集测量结果的高阶矩以计算方差与偏度,既简单又廉价。
运行时间测量必须用统计方法加固。
第 17 章:类函数宏
只要可能,就优先选择内联函数,而非类函数宏。
类函数宏应当为复杂任务提供简单接口。
宏替换发生在很早的翻译阶段,早于对组成程序的记号赋予任何其他解释。
类函数宏后面如果没有 (),就不会展开。
LINE 中的行号可能无法装入 int。
使用 LINE 本身就带有危险。
使用运算符 # 字符串化时,不会展开实参中的宏。
嵌套宏定义可以多次展开宏实参。
传给可变形参时,所有算术类型都会像参与算术操作时那样转换;唯一例外是 float 实参,它会转换为 double。
可变实参函数必须获得有关可变列表中每个实参类型的有效信息。
除非把每个实参强制为特定类型,否则使用可变实参函数不具备可移植性。
新接口应避免使用可变实参函数。
va_arg 机制无法取得 va_list 的长度。
可变实参函数需要一项专门约定来表示列表长度。
第 18 章:类型泛型编程
_Generic 表达式的结果类型,就是所选表达式的类型。
把 _Generic 与内联函数结合使用,可以增加优化机会。
_Generic 中的所有候选表达式
_Generic 表达式中的类型表达式只应采用无限定类型,不应采用数组类型或函数类型。
_Generic 表达式中的类型表达式必须指代彼此不相容的类型。
_Generic 表达式中的类型表达式不能是指向 VLA 的指针。
在宏内部,用已经写入文档的命名约定保护局部对象。
必须确保类型一致时,使用 auto 定义。
声明对象时,优先使用 auto,而不是 typeof。
第 19 章:控制流的变化
函数中的副作用可能导致不确定的结果。
任何运算符的具体运算,都后序于其所有操作数的求值。
通过赋值、递增或递减运算符更新对象的效果,后序于其操作数的求值。
函数调用相对于调用方的所有求值都有定序关系。
数组类型或结构体类型的初始化列表表达式彼此不确定定序。
每次迭代都会定义局部对象的一个新实例。
goto 只应用于控制流的特殊变化。
每次函数调用都会定义局部对象的一个新实例。
longjmp 绝不会返回调用方。
通过正常控制流到达时,setjmp 调用把调用位置标记为跳转目标,并返回 0。
离开 setjmp 调用的作用域,会使跳转目标失效。
longjmp 调用把控制直接转移到 setjmp 设置的位置,效果如同后者返回了实参 condition。
传给 longjmp 的 condition 实参如果是 0,会替换为 1。
setjmp 只能用于条件语句控制表达式内的简单比较。
优化与 setjmp 调用的相互作用很糟糕。
跨越 longjmp 发生修改的对象必须带 volatile 限定。
每次访问 volatile 对象时,都从内存重新加载。
每次修改 volatile 对象时,都把它存入内存。
jmp_buf 的 typedef 隐藏了一个数组类型。
C 的信号处理接口非常有限,只应用于最简单的情形。
信号处理函数可以在执行的任何位置介入。
从信号处理函数返回后,执行在中断发生的确切位置恢复。
任何一条 C 语句都可能对应多条处理器指令。
信号处理函数需要操作不可中断的类型。
sig_atomic_t 类型的对象不应用作计数器。
除非另有明确规定,否则 C 库函数并非异步信号安全。
第 20 章:线程
如果线程 T0 写入非原子对象,而另一个线程 T1 同时读取或写入该对象,执行就会失败。
在不同线程的执行看来,原子对象上的标准操作不可分割,并且可线性化。
声明原子对象时,请使用说明符句法 _Atomic(T)。
不存在原子数组类型。
原子对象是强制排除竞态条件的首选工具。
正确初始化的 FILE* 可以由多个线程无竞态地使用。
并发写操作应当一次打印完整的一行。
销毁和解分配共享动态对象需要格外谨慎。
通过函数实参传递线程特定数据。
在线程的局部对象中保存线程特定状态。
thread_local 对象在每个线程中都有一个独立实例。
如果能在编译期确定初始化,请使用 thread_local。
互斥量操作提供线性化性。
每个互斥量都必须用 mtx_init 初始化。
持有非递归互斥量的线程,不得再为该互斥量调用任何锁定函数。
持有递归互斥量的线程调用 mtx_unlock 的次数达到获取锁的次数后,该互斥量才会释放。
线程终止之前,必须释放其锁定的互斥量。
线程只能对自己持有的互斥量调用 mtx_unlock。
每次成功锁定互斥量,都恰好对应一次 mtx_unlock 调用。
互斥量必须在生存期末尾销毁。
从 cnd_t 等待返回后,必须再次检查表达式。
同一个条件量在同一时刻只能与一个互斥量配合使用。
cnd_t 必须动态初始化。
cnd_t 必须在生存期末尾销毁。
从 main 返回或调用 exit 会终止所有线程。
线程阻塞于 mtx_t 或 cnd_t 时,会释放处理资源。
需要锁定多个互斥量的临界区,应当始终以相同顺序锁定这些互斥量。
为了避免死锁,请优先使用 cnd_timedwait,而不是 cnd_wait。
第 21 章:原子访问与内存一致性
每次求值都有作用。
如果 F 先序于 E,那么
对原子对象 X 的所有修改按某种顺序进行,该顺序与每个操作 X 的线程内部的“先序于”关系一致。
如果线程 E 读到了另一线程 F 写入的值,那么 E 与 F 同步。
如果 F 与 E 同步,那么所有先于 F 发生的作用 X,都必须在所有发生于 E 之后的求值 G 处可见。
只有存在一条把两次求值连接起来的定序与同步链条,才能断定一次求值先于另一次发生。
如果求值 F 先于 E 发生,那么所有已知先于 F 发生的作用,也已知先于 E 发生。
受同一个互斥量保护的临界区顺序发生。
在受互斥量 mut 保护的临界区中,此前所有受 mut 保护的临界区的作用均可见。
cnd_wait 和 cnd_timedwait 对互斥量具有释放—获取语义。
cnd_signal 和 cnd_broadcast 调用通过互斥量同步。
cnd_signal 和 cnd_broadcast 应当在受等待方所用同一互斥量保护的临界区内调用。
具有顺序一致性的所有原子操作,都处于同一个全局修改顺序中,与它们作用于哪个原子对象无关。
除非另有说明,所有原子运算符及函数接口都具有顺序一致性。
名称附加 _explicit 的原子对象同步函数接口,允许指定一致性模型。
技术附录
只在过渡阶段使用头文件 c23-fallback.h,直到平台完整支持 C23。
头文件 c23-fallback.h 只能以受限能力模拟部分 C23 特性。
使用最新的编译器版本。
Clang 18 以前的版本会禁用 [u]int128_t 支持。