要点汇总
导言
要点 #1
C 与 C++ 是两种不同的语言:不要混用,也不要混淆。
第 1 章 入门
要点 1 #2
不要恐慌。
要点 1.1 #1
C 是一种命令式编程语言。
要点 1.2 #1
C 是一种编译型编程语言。
要点 1.2 #2
正确的 C 程序可以在不同平台之间移植。
要点 1.2 #3
C 程序应当干净利落地完成编译,不产生警告。
第 2 章 程序的基本结构
要点 2.1 #1
同一个标点字符可以用于多种不同目的。
要点 2.2 #1
程序中的所有标识符都必须声明。
要点 2.2 #2
一个标识符可以有多个相互一致的声明。
要点 2.2 #3
声明绑定于其出现的作用域。
要点 2.3 #1
声明说明标识符,定义说明对象。
要点 2.3 #2
对象在初始化的同时得到定义。
要点 2.3 #3
初始化器中缺少的元素默认为 0。
要点 2.3 #4
对于含有 n 个元素的数组,第一个元素的索引是 0,最后一个元素的索引是 n-1。
要点 2.3 #5
每个对象或函数都必须恰好有一个定义。
要点 2.4.1 #1
范围迭代应当用 for 语句编写。
要点 2.4.1 #2
循环对象应当定义在 for 的起始部分中。
第 3 章 一切都与控制有关
要点 3.1 #1
值 0 表示逻辑假。
要点 3.1 #2
任何不等于 0 的值都表示逻辑真。
要点 3.1 #3
不要与 0、false 或 true 作比较。
要点 3.1 #4
所有标量都有真值。
要点 3.3 #1
case 值必须是整数常量表达式。
要点 3.3 #2
每个 switch 语句中的 case 值必须唯一。
要点 3.3 #3
case 标签不得跳过对象定义。
第 4 章 表达计算
要点 4.1 #1
size_t 类型表示范围 [0, SIZE_MAX] 内的值。
要点 4.2.1 #1
无符号算术始终是良好定义的。
要点 4.2.1 #2
如果数学上的正确结果可以表示为 size_t,那么 size_t 上的 +、- 和 * 运算就会给出该结果。
要点 4.2.2 #1
对于无符号值,a == (a/b) * b + (a%b)。
要点 4.2.2 #2
只有第二个操作数不为 0 时,无符号 / 和 % 才是良好定义的。
要点 4.2.2 #3
size_t 算术会隐式地以 SIZE_MAX + 1 为模进行计算。
要点 4.2.2 #4
发生溢出时,无符号算术会回绕。
要点 4.2.2 #5
无符号 / 和 % 的结果始终小于操作数。
要点 4.2.2 #6
无符号 / 和 % 不会溢出。
要点 4.3 #1
一个运算符的所有字符必须彼此直接相连。
要点 4.3 #2
值表达式中的副作用是邪恶的。
要点 4.3 #3
绝不要在一个语句中修改一个以上的对象。
要点 4.4.1 #1
比较运算符返回值 false 或 true。
要点 4.4.2 #1
逻辑运算符返回值 false 或 true。
要点 4.6 #1
&&、||、?: 和 , 首先对第一个操作数求值。
要点 4.6 #2
不要使用 , 运算符。
要点 4.6 #3
大多数运算符不会为其操作数定序。
要点 4.6 #4
函数调用不会为其实参表达式定序。
要点 4.6 #5
表达式中的函数调用不应带有副作用。
第 5 章 基本值与数据
要点 5 #1
C 程序主要针对值进行推理,而不是针对值的表示。
要点 5.1.1 #1
所有值都是数,或者可以转换成数。
要点 5.1.2 #1
所有值都有静态确定的类型。
要点 5.1.2 #2
一个值可以进行哪些操作,由其类型决定。
要点 5.1.2 #3
一个值的类型决定所有操作的结果。
要点 5.1.3 #1
一个类型的二进制表示决定所有操作的结果。
要点 5.1.3 #2
一个类型的二进制表示可以观察。
要点 5.1.3 #3
程序的执行表现得仿佛遵循抽象状态机。
要点 5.1.4 #1
类型决定优化机会。
要点 5.2 #1
进行算术运算之前,窄整数会提升为 signed int。
要点 5.2 #2
四类基本类型各自都有三种彼此不同、未经提升的类型。
要点 5.2 #3
使用 size_t 表示大小、基数或序数。
要点 5.2 #4
对于不能为负的小数量,使用 unsigned。
要点 5.2 #5
对于带正负号的小数量,使用 signed。
要点 5.2 #6
对于带正负号的较大差值,使用 ptrdiff_t。
要点 5.2 #7
浮点计算使用 double。
要点 5.2 #8
复数计算使用 double complex。
要点 5.3 #1
连续的字符串字面量会连接起来。
要点 5.3 #2
数值字面量绝不会是负数。
要点 5.3 #3
十进制整数字面量是有符号的。
要点 5.3 #4
十进制整数字面量的类型,是能够容纳它的三种有符号类型中的第一种。
要点 5.3 #5
同一个值可以具有不同类型。
要点 5.3 #6
不要用二进制、八进制或十六进制字面量表示负值。
要点 5.3 #7
负值使用十进制字面量。
要点 5.3 #8
不同字面量可以具有相同的值。
要点 5.3 #9
十进制浮点字面量的实际值可能不同于其字面值。
要点 5.3 #A
字面量具有值、类型和二进制表示。
要点 5.3.1 #1
I 保留用于表示虚数单位。
要点 5.4 #1
一元 - 和 + 的类型就是其操作数提升后的类型。
要点 5.4 #2
避免窄化转换。
要点 5.4 #3
不要在算术运算中使用窄类型。
要点 5.4 #4
避免对符号性不同的操作数进行运算。
要点 5.4 #5
只要可行,就使用无符号类型。
要点 5.4 #6
选择算术类型时,应当使隐式转换无害。
要点 5.5 #1
所有对象都应当初始化。
要点 5.5 #2
所有聚合数据类型都应使用指派初始化器。
要点 5.5 #3
{ } 是适用于所有对象的有效初始化器。
要点 5.6 #1
所有具有特定含义的常量都必须命名。
要点 5.6 #2
含义不同的所有常量都必须区分开来。
要点 5.6.1 #1
const 限定类型的对象是只读的。
要点 5.6.1 #2
字符串字面量是只读的。
要点 5.6.2 #1
枚举常量要么具有显式值,要么具有按位置确定的值。
要点 5.6.2 #2
如果简单枚举类型的所有枚举常量都能装入 signed int,它们就采用该类型。
要点 5.6.2 #3
整数常量表达式只能对声明为 constexpr 的对象求值。
要点 5.6.2 #4
如果枚举常量无法装入 signed int,那么只要可能,枚举类型就会调整为能够存储所有枚举常量的类型。
要点 5.6.2 #5
如果枚举常量无法装入 signed int,这些常量就采用相应枚举类型。
要点 5.6.2 #6
如果枚举常量可能无法全部装入 signed int,请说明枚举类型的底层整数类型。
要点 5.6.3 #1
宏名全部使用大写字母。
要点 5.6.4 #1
复合字面量定义一个对象。
要点 5.6.4 #2
不要把末尾分号隐藏在宏中。
要点 5.6.4 #3
把宏的续行标记向右缩进到同一列。
要点 5.6.5 #1
constexpr 的初始化器必须恰好适配。
要点 5.7 #1
同一个值可以具有不同的二进制表示。
要点 5.7.1 #1
任何整数类型的最大值都具有
要点 5.7.1 #2
无符号整数类型上的算术由其精度决定。
要点 5.7.3 #1
移位运算的第二个操作数必须小于精度。
要点 5.7.5 #1
正值的表示与符号性无关。
要点 5.7.5 #2
抽象状态机一旦到达未定义状态,就不能再对后续执行作任何假设。
要点 5.7.5 #3
你有责任避免所有操作中的未定义行为。
要点 5.7.5 #4
如果程序状态到达具有未定义行为的操作,执行就已经失败。
要点 5.7.5 #5
有符号算术可能以糟糕的方式陷入异常。
要点 5.7.5 #6
INT_MIN < -INT_MAX。
要点 5.7.5 #7
有符号算术中的取负可能溢出。
要点 5.7.5 #8
位运算使用无符号类型。
要点 5.7.6 #1
如果提供了类型 uintN_t,它就是宽度和精度都恰好为 N 位的无符号整数类型。
要点 5.7.6 #2
如果提供了类型 intN_t,它就是宽度恰好为 N 位、精度为 N-1 位的有符号类型。
要点 5.7.6 #3
如果某个 N 值对应的所需性质类型存在,就必须提供 intN_t 和 uintN_t。
要点 5.7.6 #4
对于提供的任何定宽类型,还会提供宽度 _WIDTH、最小值 _MIN(仅有符号类型)、最大值 _MAX 以及字面量 _C 宏。
要点 5.7.8 #1
浮点运算既不满足结合律、交换律,也不满足分配律。
要点 5.7.8 #2
绝不要比较浮点值是否相等。
第 6 章 派生数据类型
要点 6.1 #1
数组不是指针。
要点 6.1.2 #1
数组用于条件时求值为真。
要点 6.1.2 #2
存在数组对象,但不存在数组值。
要点 6.1.2 #3
数组不能比较。
要点 6.1.2 #4
数组不能赋值。
要点 6.1.3 #1
VLA 只能采用默认初始化器。
要点 6.1.3 #2
VLA 不能在函数外声明。
要点 6.1.3 #3
FLA 的长度由整数常量表达式或初始化器决定。
要点 6.1.3 #4
数组长度说明必须严格为正。
要点 6.1.3 #5
如果长度不是整数常量表达式,数组就是 VLA。
要点 6.1.3 #6
数组 A 的长度是 (sizeof A)/(sizeof A[0])。
要点 6.1.4 #1
函数数组形参的最内层维度会丢失。
要点 6.1.4 #2
不要对函数的数组形参使用 sizeof 运算符。
要点 6.1.4 #3
数组形参表现得仿佛数组按引用传递。
要点 6.1.5 #1
字符串是以 0 终止的 char 数组。
要点 6.1.5 #2
对非字符串使用字符串函数会导致程序失败。
要点 6.2 #1
指针是不透明对象。
要点 6.2 #2
指针要么有效、要么为空、要么无效。
要点 6.2 #3
用 nullptr 初始化或赋值会使指针为空。
要点 6.2 #4
在逻辑表达式中,空指针求值为假。
要点 6.2 #5
无效指针会导致程序失败。
要点 6.2 #6
始终初始化指针。
要点 6.3.1 #1
省略的 struct 初始化器会把相应成员强制设为 0。
要点 6.3.1 #2
struct 形参按值传递。
要点 6.3.1 #3
结构体可以赋值。
要点 6.3.1 #4
结构体不能用 == 或 != 比较。
要点 6.3.1 #5
结构体布局是一项重要的设计决策。
要点 6.3.3 #1
嵌套声明中的所有 struct 声明具有相同的可见作用域。
要点 6.3.4 #1
任何结构体成员之后都可能存在填充。
要点 6.3.4 #2
结构体开头不存在填充。
要点 6.3.4 #3
说明位域时,不要使用不带修饰的 int。
要点 6.3.4 #4
宽度为 N 的数值位域应使用 _BitInt(N) 类型。
要点 6.3.4 #5
宽度为 1 的标志位域应使用 bool 类型。
要点 6.4 #1
在 typedef 中前向声明 struct 时,使用与标记名相同的标识符。
要点 6.4 #2
typedef 只为类型创建别名,绝不会创建新类型。
要点 6.4 #3
以 _t 结尾的标识符名称是保留的。
第 7 章 函数
要点 7.1 #1
所有函数都必须有原型。
要点 7.1 #2
函数只有一个入口,却可以有多个返回点。
要点 7.1 #3
函数返回必须与其类型一致。
要点 7.1 #4
到达函数体末尾等价于执行不带表达式的 return 语句。
要点 7.1 #5
只有 void 函数才允许到达函数体末尾。
要点 7.2 #1
main 的返回值使用 EXIT_SUCCESS 和 EXIT_FAILURE。
要点 7.2 #2
到达 main 末尾等价于返回 EXIT_SUCCESS。
要点 7.2 #3
调用 exit(s) 等价于在 main 中求值 return s。
要点 7.2 #4
exit 绝不会失败,也绝不会返回调用方。
要点 7.2 #5
所有命令行实参都以字符串形式传递。
要点 7.2 #6
argv[0] 指向调用程序时所用的名称。
要点 7.2 #7
argv[argc] 是空指针。
要点 7.3 #1
明确写出函数的所有先决条件。
要点 7.3 #2
递归函数首先检查终止条件。
要点 7.3 #3
在封装函数中保证递归函数的先决条件。
要点 7.3 #4
多重递归可能导致指数级计算时间。
要点 7.3 #5
糟糕的算法永远无法产生高性能实现。
要点 7.3 #6
改进算法可以显著提升性能。
第 8 章 C 库函数
要点 8.1.3 #1
失败始终是一种可能。
要点 8.1.3 #2
检查库函数返回值中是否有错误。
要点 8.1.3 #3
快速失败、尽早失败、经常失败。
要点 8.1.4 #1
以 _s 结尾的标识符名称是保留的。
要点 8.1.5 #1
如果执行平台不满足先决条件,必须中止编译。
要点 8.1.5 #2
预处理条件中只对宏和整数字面量求值。
要点 8.1.5 #3
预处理条件中的未知标识符求值为 0。
要点 8.4.1 #1
不透明类型通过函数接口说明。
要点 8.4.1 #2
不要依赖不透明类型的实现细节。
要点 8.4.1 #3
puts 和 fputs 对行末的处理不同。
要点 8.4.3 #1
文本输入和输出会转换数据。
要点 8.4.3 #2
行末编码有三种常用转换。
要点 8.4.3 #3
文本行不应含有尾随空白。
要点 8.4.4 #1
printf 调用中后续实参必须与格式说明符精确对应。
要点 8.4.4 #2
打印整数值时使用 "%d" 和 "%u" 格式。
要点 8.4.4 #3
打印位模式时使用 "%b" 或 "%x" 格式。
要点 8.4.4 #4
打印浮点值时使用 "%g" 格式。
要点 8.4.4 #5
使用不适当的格式说明符或修饰符会造成未定义行为。
要点 8.4.4 #6
对于稍后还要读取的转换,使用 "%+d"、"%#X" 和 "%a"。
要点 8.4.5 #1
不要使用 gets。
要点 8.4.5 #2
fgetc 返回 int,这样除了所有有效字符外,还能编码特殊错误状态 EOF。
要点 8.4.5 #3
只有读取失败之后才能检测文件结束。
要点 8.5 #1
数值编码字符的解释取决于执行字符集。
要点 8.5.1 #1
不要用字符串转换函数确定数的边界。
要点 8.5.1 #2
不要用字符串转换函数扫描源自数字字面量的数。
要点 8.8 #1
程序正常终止时,应当从 main 返回。
要点 8.8 #2
可能终止正常控制流的函数应使用 exit。
要点 8.8 #3
除非必须阻止库清理代码执行,否则不要使用 exit 以外的函数终止程序。
要点 8.8 #4
尽可能多用断言来确认运行时性质。
要点 8.8 #5
生产构建使用 NDEBUG 关闭所有断言。
第 9 章 风格
要点 9 #1
所有 C 代码都必须可读。
要点 9 #2
短期记忆容量和视野都很有限。
要点 9 #3
编码风格不是品味问题,而是文化问题。
要点 9 #4
每个成熟项目都构成自己的文化空间。
要点 9.1 #1
为空白和其他文本格式选择一套一致的策略。
要点 9.1 #2
让文本编辑器自动以正确格式排版代码。
要点 9.2 #1
为所有标识符选择一致的命名策略。
要点 9.2 #2
头文件中可见的任何标识符都必须符合标准。
要点 9.2 #3
不要污染标识符的全局空间。
要点 9.2 #4
名称必须容易辨认,也能迅速彼此区分。
要点 9.2 #5
命名是一种创造行为。
要点 9.2 #6
文件作用域标识符必须完整表达含义。
要点 9.2 #7
类型名标识一个概念。
要点 9.2 #8
全局常量标识一种制品。
要点 9.2 #9
全局对象标识状态。
要点 9.2 #A
函数或函数式宏标识一个动作。
要点 9.3 #1
项目的自然语言应当选择成能够适应大多数参与者的语言。
要点 9.3 #2
只有在字母按 C 规范化形式映射到自身时,才允许用于标识符。
要点 9.3 #3
只有直接源自自然语言,或者与所有自然语言都显著不同的字母,才应用于标识符。
要点 9.3 #4
只有在彼此明显不同的情况下,才在标识符中使用来自不同文字体系的字母或十进制数字变体。
要点 9.3 #5
在标识符中使用下标或上标字母不具备可移植性。
第 10 章 组织和文档化
要点 10 #1
函数接口描述做什么。
要点 10 #2
接口注释记录函数为了什么。
要点 10 #3
函数代码展示函数如何组织。
要点 10 #4
代码注释解释函数细节以何种方式实现。
要点 10 #5
分离接口与实现。
要点 10 #6
记录接口;解释实现。
要点 10.1 #1
完整地记录接口。
要点 10.1 #2
把代码组织成语义联系紧密的单元。
要点 10.2 #1
按字面直接实现。
要点 10.2 #2
控制流必须显而易见。
要点 10.2.1 #1
宏不应以出人意料的方式改变控制流。
要点 10.2.1 #2
函数式宏在句法上应当表现得像函数调用。
要点 10.2.2 #1
函数形参按值传递。
要点 10.2.2 #2
不提倡使用全局对象。
要点 10.2.2 #3
只要可能,就把小任务表达为纯函数。
要点 10.2.3 #1
属性中的标识符可以被预处理替换。
要点 10.2.3 #2
头文件中的属性应使用双下划线形式。
第 11 章 指针
要点 11.1.1 #1
程序执行对无效指针或空指针应用 * 时会失败。
要点 11.1.2 #1
有效指针指向引用类型数组的第一个元素。
要点 11.1.2 #2
无法从指针还原数组对象的长度。
要点 11.1.2 #3
指针不是数组。
要点 11.1.3 #1
只能对指向同一数组对象中元素的指针作减法。
要点 11.1.3 #2
所有指针差都具有 ptrdiff_t 类型。
要点 11.1.3 #3
用 ptrdiff_t 编码位置或大小的有符号差值。
要点 11.1.3 #4
打印指针值时,把它转换为 void* 并使用格式 %p。
要点 11.1.4 #1
指针具有真值。
要点 11.1.4 #2
尽早把指针对象设为空。
要点 11.1.4 #3
程序执行访问其类型对应表示为非值表示的对象时会失败。
要点 11.1.4 #4
解引用时,指向的对象必须具有所指代的类型。
要点 11.1.4 #5
指针必须指向有效对象、有效对象后一个位置,或者为空。
要点 11.1.4 #6
程序执行计算出数组对象边界之外(也不正好在后一个位置)的指针值时会失败。
要点 11.1.5 #1
使用 nullptr,不要使用 NULL。
要点 11.2 #1
不要把指针类型隐藏在 typedef 中。
要点 11.3.1 #1
表达式 A[i] 与 *(A+i) 等价。
要点 11.3.1 #2
对数组 A 求值会返回 &A[0]。
要点 11.3.2 #1
在函数声明中,任何数组形参都会改写成指针。
要点 11.3.2 #2
只有数组形参的最内层维度会改写。
要点 11.3.2 #3
长度形参应当声明在数组形参之前。
要点 11.3.2 #4
传给函数的数组实参是否有效,必须由程序员保证。
要点 11.4 #1
函数名后面不带圆括号时,会退化为指向函数入口的指针。
要点 11.4 #2
函数指针必须按其精确类型使用。
要点 11.4 #3
函数调用运算符 (···) 作用于函数指针。
第 12 章 指针的高级用法
要点 12 #1
基类型不同的指针类型彼此不同。
要点 12.1 #1
按照定义,sizeof(char) 为 1。
要点 12.1 #2
任何对象 A 都可以视为 unsigned char[sizeof A]。
要点 12.1 #3
指向字符类型的指针具有特殊性。
要点 12.1 #4
字符和字符串数据使用 char 类型。
要点 12.1 #5
使用 unsigned char 类型作为所有对象类型的原子组成单元。
要点 12.1 #6
sizeof 运算符可以应用于对象和对象类型。
要点 12.1 #7
所有 T 类型对象的大小由 sizeof(T) 给出。
要点 12.2 #1
算术类型各表示数字在内存中的排列顺序是实现定义的。
要点 12.2 #2
在大多数体系结构上,CHAR_BIT 为 8,UCHAR_MAX 为 255。
要点 12.3 #1
除字符类型外,只有基类型相同的指针才能形成别名。
要点 12.3 #2
避免使用 & 运算符。
要点 12.4 #1
任何对象指针都能与 void* 相互转换。
要点 12.4 #2
对象具有存储、类型和值。
要点 12.4 #3
把对象指针转换为 void*,再转换回同一类型,是恒等操作。
要点 12.4 #4
避免使用 void*。
要点 12.5 #1
不要使用强制转换。
要点 12.6 #1
对象必须通过其有效类型,或者通过指向字符类型的指针访问。
要点 12.6 #2
只要字节表示构成访问类型的有效值,具有有效联合体类型的对象中任何成员都可以随时访问。
要点 12.6 #3
命名对象或复合字面量的有效类型就是其声明的类型。
要点 12.6 #4
命名对象和复合字面量必须通过其声明类型,或者通过指向字符类型的指针访问。
第 13 章 存储
要点 13.1 #1
调用分配函数时,只使用严格大于零的大小。
要点 13.1 #2
分配失败会得到空指针。
要点 13.1 #3
优先使用 strndup,而不是 strdup。
要点 13.1 #4
不要对 malloc 及类似函数的返回值作强制转换。
要点 13.1 #5
通过 malloc 分配的存储未经初始化,也没有类型。
要点 13.1.1 #1
malloc 通过返回空指针值表示失败。
要点 13.1.2 #1
每次分配都必须对应一次 free。
要点 13.1.2 #2
每次 free 都必须对应一次 malloc、calloc、aligned_alloc 或 realloc。
要点 13.1.2 #3
只有对 malloc、calloc、aligned_alloc 或 realloc 原样返回的指针,才能调用 free。
要点 13.1.3 #1
带柔性数组成员的结构体对象必须具有足够存储,才能访问整个结构体。
要点 13.1.3 #2
长度成员与柔性数组成员之间的一致性必须手动维护。
要点 13.2 #1
标识符只在自身作用域内可见,并从声明处开始可见。
要点 13.2 #2
下级作用域中的同名标识符可以遮蔽一个标识符的可见性。
要点 13.2 #3
每次定义命名对象都会创建一个全新且不同的对象。
要点 13.2 #4
只读对象字面量可能彼此重叠。
要点 13.2 #5
对象具有生存期,不能在生存期之外访问。
要点 13.2 #6
程序执行引用生存期之外的对象时会失败。
要点 13.2 #7
复合字面量与同一上下文中以相同存储类声明的对象具有相同生存期。
要点 13.2.1 #1
具有静态存储期的对象始终会初始化。
要点 13.2.2 #1
除非自动对象是 VLA 或临时对象,否则其生存期对应于定义所在块的执行期间。
要点 13.2.2 #2
每次递归调用都会创建自动对象的新局部实例。
要点 13.2.2 #3
不能对声明为 register 的对象使用 & 运算符。
要点 13.2.2 #4
声明为 register 的对象不能形成别名。
要点 13.2.2 #5
性能关键代码中,不是数组的局部对象应声明为 register。
要点 13.2.2 #6
采用存储类 register 的数组毫无用处。
要点 13.2.2 #7
临时生存期对象是只读的。
要点 13.2.2 #8
临时生存期在外围完整表达式末尾结束。
要点 13.3 #1
对于不是 VLA 的对象,生存期从进入定义的作用域时开始,离开该作用域时结束。
要点 13.3 #2
每次遇到定义时,都会对自动对象和复合字面量的初始化器求值。
要点 13.3 #3
对于 VLA,生存期从遇到定义时开始,在离开可见作用域时结束。
要点 13.4 #1
具有静态或线程存储期的对象默认会初始化。
要点 13.4 #2
具有自动存储期或已分配存储期的对象必须显式初始化。
要点 13.4 #3
为每一种数据类型系统地提供初始化函数。
第 14 章 进阶处理与输入输出
要点 14.1 #1
字符串 strto... 转换函数不具备 const 安全性。
要点 14.1 #2
memchr 和 strchr 搜索函数的函数接口不具备 const 安全性。
要点 14.1 #3
memchr 和 strchr 搜索函数的类型泛型接口具备 const 安全性。
要点 14.1 #4
strspn 和 strcspn 搜索函数具备 const 安全性。
要点 14.1 #5
sprintf 没有采取任何防止缓冲区溢出的措施。
要点 14.1 #6
格式化长度未知的输出时,请使用 snprintf。
要点 14.3 #1
多字节字符不包含空字节。
要点 14.3 #2
多字节字符串以空字符终止。
要点 14.5 #1
一个码点的多字节 mb 编码会一次性写入输出字符串。
要点 14.5 #2
一个码点的多字节 mb 编码可以从输入中分段收集。
要点 14.6 #1
凡是使用 fread 或 fwrite 的流,都应当以二进制模式打开。
要点 14.6 #2
以二进制模式写出的文件不能跨平台移植。
要点 14.6 #3
fseek 和 ftell 不适合非常大的文件偏移量。
第 15 章 程序失败
要点 15.1.1 #1
程序执行只能进行在底层类型范围内具有数学定义的算术运算。
要点 15.1.1 #2
平台的浮点环境决定哪些浮点运算会导致程序失败。
要点 15.1.1 #3
指针操作应当始终保持在数组对象的边界之内。
要点 15.1.1 #4
只要可能,就使用数组索引,而不是把指针算术与解引用结合起来。
要点 15.1.4 #1
除非必须,否则不要转换指针。
要点 15.1.4 #2
始终使用函数定义所采用的原型调用函数。
要点 15.1.4 #3
通过函数名称调用函数。
要点 15.1.6 #1
不要在 bool 对象中存储 0 和 1 以外的值。
要点 15.1.6 #2
不要直接更改对象的表示字节。
要点 15.1.7 #1
只有掌握证明时才使用 unreachable()。
要点 15.1.7 #2
除了 unreachable(),不要用其他操作标记绝不会采用的控制路径。
要点 15.3.2 #1
不要在同一个算术表达式中读取并修改同一个对象。
要点 15.4 #1
程序执行在有限状态集中循环且没有可观察副作用时,已经失败。
要点 15.5 #1
保证可能失败的操作所需的全部先决条件。
要点 15.5 #2
对于可能耗尽资源的操作,应检查其返回值是否表示错误。
要点 15.5 #3
只有谨慎设计算法,才能避免不幸事件。
要点 15.6 #1
goto 标签在包含它的整个函数中可见。
要点 15.6 #2
goto 只能跳转到同一函数内的标签。
要点 15.6 #3
goto 不应跳过对象初始化。
第 16 章 性能
要点 16 #1
过早优化是万恶之源。
要点 16 #2
不要用安全性换取性能。
要点 16 #3
优化器足够聪明,能够消除未使用的初始化。
要点 16 #4
函数指针实参的不同记法会产生相同的二进制代码。
要点 16 #5
不取得局部对象的地址有助于优化器,因为这样会抑制别名。
要点 16.1 #1
内联可以带来大量优化机会。
要点 16.1 #2
增加一个不含 inline 关键字的兼容声明,可以保证在当前翻译单元中生成函数符号。
要点 16.1 #3
内联函数定义在所有翻译单元中都可见。
要点 16.1 #4
内联定义应放在头文件中。
要点 16.1 #5
不含 inline 的附加声明恰好放在一个翻译单元中。
要点 16.1 #6
只有认为函数已经稳定时,才把它公开为 inline。
要点 16.1 #7
内联函数中的所有局部标识符都应当用适当的命名约定保护。
要点 16.1 #8
inline 函数不能按名称访问静态函数。
要点 16.1 #9
inline 函数不能按名称访问可修改的静态对象。
要点 16.1 #A
inline 函数不能定义可修改的静态对象。
要点 16.2 #1
restrict 限定指针必须提供独占访问。
要点 16.2 #2
restrict 限定会约束函数调用方。
要点 16.3 #1
所有纯函数都应具有属性 [[unsequenced]]。
要点 16.3 #2
具有属性 [[unsequenced]] 的函数不得读取非常量全局对象或系统状态。
要点 16.3 #3
一般来说,使用浮点算术的函数不是纯函数,不得具有属性 [[unsequenced]]。
要点 16.3 #4
具有属性 [[unsequenced]] 的函数不得对全局对象或系统状态施加可见修改。
要点 16.3 #5
通过 errno 返回可能错误的函数不是纯函数,不得具有属性 [[unsequenced]]。
要点 16.3 #6
改变浮点状态的编译指示只在当前作用域内局部生效。
要点 16.3 #7
类型属性会在当前作用域内累积。
要点 16.3 #8
只要恢复到原值,具有属性 [[reproducible]] 的函数可以临时修改全局状态。
要点 16.3 #9
对于具有 [[unsequenced]] 或 [[reproducible]] 属性的函数,应以 restrict 注解指针形参。
要点 16.3 #A
具有属性 [[unsequenced]] 的函数不得修改局部静态状态,即使通过其他函数调用也不行。
要点 16.3 #B
具有属性 [[reproducible]] 的函数,只有在外部无法观察相应状态时,才能修改局部静态状态。
要点 16.4 #1
不要臆测代码性能;应当严格验证。
要点 16.4 #2
评估算法复杂度需要证明。
要点 16.4 #3
评估代码性能需要测量。
要点 16.4 #4
所有测量都会引入偏差。
要点 16.4 #5
插桩会改变编译期和运行时性质。
要点 16.4 #6
运行时间的相对标准差必须处于较低的百分比范围。
要点 16.4 #7
收集测量值的高阶矩以计算方差和偏度,既简单又廉价。
要点 16.4 #8
必须用统计方法强化运行时间测量。
第 17 章 宏与可变实参
要点 17 #1
只要可能,就优先使用内联函数,而不是函数式宏。
要点 17 #2
函数式宏应当为复杂任务提供简单接口。
要点 17.1 #1
宏替换发生在较早的翻译阶段,早于对组成程序的记号作出任何其他解释。
要点 17.1 #2
函数式宏后面如果没有 (),就不会展开。
要点 17.3 #1
__LINE__ 中的行号可能无法装入 int。
要点 17.3 #2
使用 __LINE__ 本身就很危险。
要点 17.3 #3
运算符 # 进行字符串化时,不会展开实参中的宏。
要点 17.3 #4
嵌套宏定义可能多次展开宏实参。
要点 17.4.2 #1
传给可变形参时,所有算术类型都会像算术运算中一样转换;唯一例外是 float 实参会转换为 double。
要点 17.4.2 #2
可变实参函数必须取得可变实参列表中每个实参类型的有效信息。
要点 17.4.2 #3
除非把每个实参都强制成特定类型,否则使用可变实参函数不具备可移植性。
要点 17.4.2 #4
新接口应避免使用可变实参函数。
要点 17.4.2 #5
va_arg 机制无法取得 va_list 的长度。
要点 17.4.2 #6
可变实参函数需要一项专门约定来说明列表长度。
第 18 章 类型泛型编程
要点 18.2 #1
_Generic 表达式的结果类型就是所选表达式的类型。
要点 18.2 #2
把 _Generic 与内联函数结合使用会增加优化机会。
要点 18.2 #3
_Generic 中的所有选择表达式 expression1 ... expressionN 都必须有效。
要点 18.2 #4
_Generic 表达式中的类型表达式应当只使用无限定类型,不能使用数组类型或函数类型。
要点 18.2 #5
_Generic 表达式中的类型表达式必须指代彼此不兼容的类型。
要点 18.2 #6
_Generic 表达式中的类型表达式不能是指向 VLA 的指针。
要点 18.3.1 #1
用有文档说明的命名约定保护宏内的局部对象。
要点 18.3.1 #2
必须保证类型一致时,使用 auto 定义。
要点 18.3.2 #1
对象声明优先使用 auto,而不是 typeof。
第 19 章 控制流的变化
要点 19.2 #1
函数中的副作用可能导致不确定结果。
要点 19.2 #2
任何运算符的具体运算,都在其全部操作数求值之后定序。
要点 19.2 #3
用赋值、递增或递减运算符更新对象的效果,在其操作数求值之后定序。
要点 19.2 #4
函数调用与调用方中的所有求值彼此定序。
要点 19.2 #5
数组或结构体类型的初始化列表表达式具有不确定顺序。
要点 19.3 #1
每次迭代都会定义局部对象的一个新实例。
要点 19.3 #2
goto 只应用于控制流的例外变化。
要点 19.4 #1
每次函数调用都会定义局部对象的一个新实例。
要点 19.5 #1
longjmp 绝不会返回调用方。
要点 19.5 #2
通过正常控制流到达时,setjmp 调用把调用位置标记为跳转目标,并返回 0。
要点 19.5 #3
离开 setjmp 调用所在的作用域会使跳转目标失效。
要点 19.5 #4
longjmp 调用会把控制直接转移到 setjmp 设置的位置,就像后者返回了 condition 实参一样。
要点 19.5 #5
传给 longjmp 的 condition 形参如果为 0,就会替换为 1。
要点 19.5 #6
setjmp 只能用于条件构造控制表达式中的简单比较。
要点 19.5 #7
优化与 setjmp 调用会产生糟糕的交互。
要点 19.5 #8
跨越 longjmp 得到修改的对象必须是 volatile。
要点 19.5 #9
每次访问 volatile 对象时,都会从内存重新加载。
要点 19.5 #A
每次修改 volatile 对象时,都会存入内存。
要点 19.5 #B
jmp_buf 的 typedef 隐藏了一个数组类型。
要点 19.6 #1
C 的信号处理接口十分有限,只应用于简单情形。
要点 19.6 #2
信号处理程序可以在执行的任何位置介入。
要点 19.6 #3
从信号处理程序返回后,执行会恰好从中断位置恢复。
要点 19.6 #4
任何 C 语句都可能对应多条处理器指令。
要点 19.6 #5
信号处理程序需要操作不可中断的类型。
要点 19.6 #6
sig_atomic_t 类型的对象不应用作计数器。
要点 19.6 #7
除非另有说明,否则 C 库函数不具备异步信号安全性。
第 20 章 线程
要点 20.1 #1
如果线程 T0 写入非原子对象,而另一个线程 T1 同时读取或写入该对象,执行就会失败。
要点 20.1 #2
从不同线程中的执行来看,对原子对象进行的标准操作不可分割,并且可线性化。
要点 20.1 #3
声明原子对象时,请使用说明符句法 _Atomic(T)。
要点 20.1 #4
不存在原子数组类型。
要点 20.1 #5
原子对象是强制排除竞态条件的首选工具。
要点 20.2 #1
正确初始化的 FILE* 可以由多个线程无竞态地使用。
要点 20.2 #2
并发写操作应当一次打印完整的一行。
要点 20.2 #3
销毁和解分配共享动态对象需要格外谨慎。
要点 20.3 #1
通过函数实参传递线程特定数据。
要点 20.3 #2
在线程的局部对象中保存线程特定状态。
要点 20.3 #3
thread_local 对象在每个线程中都有一个独立实例。
要点 20.3 #4
如果能在编译期确定初始化,请使用 thread_local。
要点 20.4 #1
互斥量操作提供线性化性。
要点 20.4 #2
每个互斥量都必须用 mtx_init 初始化。
要点 20.4 #3
持有非递归互斥量的线程,不得再为该互斥量调用任何锁定函数。
要点 20.4 #4
持有递归互斥量的线程调用 mtx_unlock 的次数达到获取锁的次数后,该互斥量才会释放。
要点 20.4 #5
线程终止之前,必须释放其锁定的互斥量。
要点 20.4 #6
线程只能对自己持有的互斥量调用 mtx_unlock。
要点 20.4 #7
每次成功锁定互斥量,都恰好对应一次 mtx_unlock 调用。
要点 20.4 #8
互斥量必须在生存期末尾销毁。
要点 20.5 #1
从 cnd_t 等待返回后,必须再次检查表达式。
要点 20.5 #2
同一个条件量在同一时刻只能与一个互斥量配合使用。
要点 20.5 #3
cnd_t 必须动态初始化。
要点 20.5 #4
cnd_t 必须在生存期末尾销毁。
要点 20.6 #1
从 main 返回或调用 exit 会终止所有线程。
要点 20.6 #2
线程阻塞于 mtx_t 或 cnd_t 时,会释放处理资源。
要点 20.7 #1
需要锁定多个互斥量的临界区,应当始终以相同顺序锁定这些互斥量。
要点 20.7 #2
为了避免死锁,请优先使用 cnd_timedwait,而不是 cnd_wait。
第 21 章 原子访问与内存一致性
要点 21 #1
每次求值都会产生效果。
要点 21.1 #1
如果 F 先序于 E,那么
要点 21.1 #2
对原子对象 X 的各次修改,会以一种与处理 X 的任何线程中的先序关系一致的顺序执行。
要点 21.1 #3
如果线程 TE 中的获取操作 E 读取了另一线程 TF 中释放操作 F 所写的值,E 就与 F 同步。
要点 21.1 #4
如果 F 与 E 同步,那么在 F 之前发生的所有效果 X,都必须在 E 之后发生的所有求值 G 中可见。
要点 21.1 #5
只有存在一条连接两次求值、由同步关系组成的定序链,才能断定一次求值先于另一次发生。
要点 21.1 #6
如果求值 F 先于 E 发生,那么已知先于 F 发生的所有效果,也都已知先于 E 发生。
要点 21.2 #1
由同一个互斥量保护的临界区依次发生。
要点 21.2 #2
在互斥量 mut 保护的临界区中,由 mut 保护的先前临界区产生的所有效果都可见。
要点 21.2 #3
cnd_wait 和 cnd_timedwait 对互斥量具有释放-获取语义。
要点 21.2 #4
cnd_signal 和 cnd_broadcast 调用通过互斥量同步。
要点 21.2 #5
cnd_signal 和 cnd_broadcast 调用应当发生在一个临界区中,该临界区由与等待方相同的互斥量保护。
要点 21.3 #1
所有具有顺序一致性的原子操作都发生在同一个全局修改顺序中,无论它们作用于哪个原子对象。
要点 21.3 #2
除非另有说明,原子类型的所有运算符和函数接口都具有顺序一致性。
要点 21.4 #1
名称末尾附加 _explicit 的原子对象同步函数接口,允许我们指定其一致性模型。
附录 A C23 兼容支持
要点 A #1
只在过渡期间使用头文件 c23-fallback.h,直至平台完整支持 C23。
要点 A #2
头文件 c23-fallback.h 只能以受限能力模拟部分 C23 功能。
附录 B 编译器支持
要点 B #1
使用最新的编译器发行版。
要点 B.4 #1
在 clang-18 以前的 Clang 版本中,禁用对 [u]int128_t 的支持。