附录整理:语法与标准库速查
这份笔记用于写程序时查阅规则、选择接口。内容以 C11 为背景,保留常用知识和关键边界;整数类型、字符编码、浮点计算与 C/C++ 差异另作一份扩展笔记。
1. 运算符优先级与结合性
下表从高到低排列。同一行中的运算符属于同一优先级。
| 级别 | 运算符或表达式形式 | 结合方向 |
|---|---|---|
| 1 | a[i]、f(...)、.、->、后缀 ++ --、复合字面量 (T){...} |
左到右 |
| 2 | 前缀 ++ --、一元 + -、! ~、解引用 *、取地址 &、sizeof、_Alignof |
右到左 |
| 3 | 强制类型转换 (T) |
右到左 |
| 4 | * / % |
左到右 |
| 5 | + - |
左到右 |
| 6 | << >> |
左到右 |
| 7 | < <= > >= |
左到右 |
| 8 | == != |
左到右 |
| 9 | 按位与 & |
左到右 |
| 10 | 按位异或 ^ |
左到右 |
| 11 | 按位或 | |
左到右 |
| 12 | 逻辑与 && |
左到右 |
| 13 | 逻辑或 || |
左到右 |
| 14 | 条件运算符 ?: |
右到左 |
| 15 | =、+= -= *= /= %=、<<= >>=、&= ^= |= |
右到左 |
| 16 | 逗号运算符 , |
左到右 |
表格便于记忆,表达式是否合法仍受语法和类型约束。条件运算符中间的表达式按加了括号的方式解析。
1.1 优先级不规定求值顺序
a() + b() * c() 先按照乘法优先的规则组织表达式,但不表示函数必定按照 b、c、a 的顺序调用。
结合性也只决定如何分组。例如 a = b = 3 按 a = (b = 3) 理解。
/* 错误示例:C11 中对 i 的修改与读取没有所需的先后关系。 */
/* printf("%d %d\n", i++, i); */
用独立语句表达需要确定顺序的修改,不要依靠参数位置或括号猜测执行顺序。
1.2 有明确先后关系的常见形式
| 形式 | 求值规则 |
|---|---|
a && b |
先计算 a;a 为假时不计算 b |
a || b |
先计算 a;a 为真时不计算 b |
a ? b : c |
先计算 a,再只计算选中的一个分支 |
(a, b) |
先计算 a,再计算 b,结果是 b 的值 |
函数实参之间的逗号是分隔符,不能套用逗号运算符的顺序规则。
1.3 常用括号形式
if ((flags & mask) == 0) {
/* 所选位全部为零。 */
}
if (p != NULL && p->next != NULL) {
/* 只有 p 有效时才访问 p->next。 */
}
flags & mask == 0 会按 flags & (mask == 0) 分组。即使记得优先级,也应在容易误读的混合表达式里主动加括号。
2. 算术、逻辑与位操作的边界
- 整数除法向零截断,如
-7 / 3 == -2,-7 % 3 == -1。 - 整数除数不能为零。商无法由结果类型表示时也有问题,例如某些实现中的
INT_MIN / -1。 - 有符号整数溢出是未定义行为;无符号整数按其范围大小取模。
!、关系运算和相等比较产生int类型的 0 或 1。逻辑运算与按位运算不能混为一谈。- 移位量必须非负,并小于提升后左操作数类型的位宽。
- 对无符号类型做位操作更容易表达预期;C11 中负有符号值右移的结果由实现定义。
混合有符号与无符号比较时,要先确认通常算术转换产生的类型。不能假设负数永远小于无符号正数。
复合赋值 a += b 会将左操作数定位一次,不能把它机械地看作所有场合都完全等价于重复书写左边的 a = a + b。
3. 类型、大小与对齐
3.1 标准保证和常见实现应分开
| 类型或性质 | 可依赖的基本规则 |
|---|---|
char |
sizeof(char) == 1;一字节的位数由 CHAR_BIT 给出 |
char、signed char、unsigned char |
三种不同类型,普通 char 的有符号性由实现决定 |
short、int |
至少支持 -32767~32767 |
long |
至少支持 -2147483647~2147483647 |
long long |
至少支持 -9223372036854775807~9223372036854775807 |
_Bool |
转换后为 0 或 1;<stdbool.h> 提供 bool、true、false |
float、double、long double |
范围与精度查 <float.h>,不能只根据名字猜字节数 |
大小满足 sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)。相邻类型可能一样大。
“一字节”在 C 中不必等于八位;CHAR_BIT 至少为 8。对象大小与类型的数值位数也不能简单等同,某些表示可能含填充位。
3.2 常用通用类型与宏
| 名称 | 头文件 | 用途 |
|---|---|---|
size_t |
<stddef.h> 等 |
对象大小与元素数量,常用 %zu 输出 |
ptrdiff_t |
<stddef.h> |
同一数组中可表示的指针差,常用 %td 输出 |
NULL |
<stddef.h> 等 |
空指针常量宏 |
offsetof(T, member) |
<stddef.h> |
结构成员相对结构起始位置的字节偏移 |
max_align_t |
<stddef.h> |
具有基本对齐要求的通用对齐类型 |
_Alignof(T) |
语言关键字 | 查询类型 T 的对齐要求 |
alignof、alignas |
<stdalign.h> |
C11 中相应关键字的宏别名 |
offsetof 不能用于位字段。结构可能含有填充,不能把各成员大小相加就当作整个结构大小。
3.3 sizeof 的两个常见误区
对非变长数组类型的普通表达式,sizeof 不会执行操作数中的副作用。变长数组相关情形需要另外考虑,不能把“不求值”当作没有例外的规则。
函数形参中的 int a[] 会调整为指针,因此函数内的 sizeof a 是指针大小。要把元素数量作为独立参数传入。
4. 存储期、作用域与链接
| 声明形式 | 作用域 | 链接 | 存储期 |
|---|---|---|---|
| 块内普通局部变量 | 块作用域 | 无 | 自动 |
块内 static 变量 |
块作用域 | 无 | 静态 |
| 文件作用域普通对象定义 | 文件作用域 | 通常外部 | 静态 |
文件作用域 static 对象 |
文件作用域 | 内部 | 静态 |
extern 声明 |
依声明位置而定 | 受声明和可见的先前声明影响 | 由所声明对象决定 |
_Thread_local 对象 |
依声明位置而定 | 依其他说明符而定 | 线程 |
| 动态分配的对象 | 不由某个局部名字的作用域决定 | 对象本身无标识符链接 | 分配存储期 |
extern int count; 通常只声明;文件作用域的 extern int count = 0; 是定义。不能说 extern 永远只能声明。
块作用域的 _Thread_local 声明需要配合 static 或 extern。线程存储期说明每个线程有自己的对象,不能用来替代共享数据的同步。
局部指针变量离开作用域,不会自动释放它指向的动态内存。反过来,局部变量的地址被保存下来,也不会延长那个局部对象的生命期。
4.1 限定符
| 写法 | 含义 |
|---|---|
const int *p |
不能通过 p 修改所指整数,p 可以改指向 |
int * const p |
p 不能改指向,所指整数可修改 |
const int * const p |
上述两项限制同时存在 |
volatile |
按实现规则保留对对象的必要访问,不是线程同步机制 |
restrict |
对特定执行期间的访问关系作出别名约定,违反约定可能产生未定义行为 |
restrict 不是“全程序绝对不许存在第二个指针”的简单规定。涉及被修改对象时,要符合它对基于受限指针的访问路径提出的要求。
5. 程序流速查
| 结构 | 关键行为 |
|---|---|
if / else |
else 对应最近一个尚未匹配的 if |
while |
先判断,可能执行零次 |
do ... while (...); |
先执行,至少一次,末尾有分号 |
for |
初始化一次;判断、循环体、更新依次重复 |
switch |
根据整数值进入标签,默认继续向下执行,直到跳出 |
break |
结束最内层的循环或 switch |
continue |
继续最内层循环;for 中仍执行更新表达式 |
return |
结束当前函数,可传回结果 |
goto |
只能跳到同一函数内的标签,常见于统一清理路径 |
switch 的 case 需要整数常量表达式,不能直接使用字符串。若 switch 位于循环内,其中的 continue 可以作用于外层循环;不能说 switch 内在语法上永远不能出现 continue。
C11 中声明不是语句。标签后若需要声明,可以先引入一个复合语句块。
6. 根据任务选择标准头文件
| 任务 | 头文件 |
|---|---|
| 字符与流、格式化输入输出 | <stdio.h> |
| 内存分配、数字转换、排序查找、终止程序 | <stdlib.h> |
| 字符串和字节块操作 | <string.h> |
| 单字节字符分类与大小写转换 | <ctype.h> |
| 整数限制、浮点特性 | <limits.h>、<float.h> |
| 按宽度表达整数、可移植格式宏 | <stdint.h>、<inttypes.h> |
| 数学函数、复数、通用数学宏 | <math.h>、<complex.h>、<tgmath.h> |
| 日期与时间、本地化 | <time.h>、<locale.h> |
| 错误码、断言 | <errno.h>、<assert.h> |
| 可变参数 | <stdarg.h> |
| 宽字符与多字节转换 | <wchar.h>、<wctype.h>、<uchar.h> |
| 非局部跳转与信号 | <setjmp.h>、<signal.h> |
| 原子与线程 | <stdatomic.h>、<threads.h> |
这份表用于定位功能。C11 的线程、原子、复数等能力有相应可选条件;头文件列表不能理解为每一种 C 环境都无条件提供全部功能。
7. 标准输入输出:先看返回值
| 接口 | 结果与关键条件 |
|---|---|
fopen |
成功返回流指针,失败返回 NULL |
fclose |
成功返回 0,失败返回 EOF;写入失败可能延迟到此时报告 |
fgetc、getchar |
返回字符值或 EOF,先用 int 接收 |
fgets |
读取成功返回缓冲区地址,保留读到的换行;失败或未读到字符便遇到结束返回 NULL |
fputs、puts |
输出字符串;puts 自动加换行,fputs 不会 |
printf、fprintf |
返回成功输出的字符数,出错返回负值 |
scanf、fscanf |
返回成功赋值的项数;匹配失败与输入结束要区分 |
fread、fwrite |
返回完整处理的元素个数,不是固定返回字节数 |
fseek、ftell |
定位或查询;文本流位置不能当作任意字节偏移使用 |
fgetpos、fsetpos |
保存和恢复流的位置状态 |
feof、ferror |
查看之前的操作是否触发结束或错误标志 |
clearerr |
清除结束与错误标志,不会自动修复底层错误 |
正确的读取循环由实际读取结果驱动:
int ch;
while ((ch = fgetc(fp)) != EOF) {
/* 处理 ch。 */
}
if (ferror(fp)) {
/* 报告读取错误。 */
}
while (!feof(fp)) 不能提前判断下一次读取是否成功。fflush() 用于输出相关场景,不能把 fflush(stdin) 当作标准 C 的清理输入方法。gets() 已不属于 C11,应使用带容量的读取方式。
7.1 格式与实参类型必须匹配
| 数据 | printf 系列 | scanf 系列的对象地址 |
|---|---|---|
int |
%d |
%d 对应 int * |
unsigned int |
%u |
%u 对应 unsigned int * |
long |
%ld |
%ld 对应 long * |
long long |
%lld |
%lld 对应 long long * |
size_t |
%zu |
%zu 对应 size_t * |
float |
%f,参数提升为 double |
%f 对应 float * |
double |
%f |
%lf 对应 double * |
long double |
%Lf |
%Lf 对应 long double * |
| 对象指针 | %p,传入 (void *)p |
%p 要求 void ** |
读取 char name[20] 时,%19s 才为末尾空字符留出空间;它遇到空白就停止,不能用来读取完整的带空格姓名。字符数组直接作为缓冲区参数,不应把它与单个字符需要的 &ch 混淆。
scanf 读取数字时,如果结果超出目标对象可表示范围,不能仅依赖返回值安全处理。需要可靠范围检查时,先读字符串,再使用 strto... 系列。
7.2 用 snprintf 控制输出容量
int n = snprintf(buffer, sizeof buffer, "%s:%d", name, port);
if (n < 0) {
/* 编码或格式化错误。 */
} else if ((size_t)n >= sizeof buffer) {
/* 输出发生截断。 */
}
非负返回值表示容量足够时本应生成的字符数,不包含末尾空字符。sizeof buffer 必须确实是数组容量,不能是形参指针的大小。
8. 字符串与内存操作
8.1 先判断是否需要空字符结尾
| 函数 | 作用 | 主要边界 |
|---|---|---|
strlen |
计算结尾空字符前的字节数 | 输入必须是有效字符串;返回 size_t |
strcmp、strncmp |
按字符编码比较 | 看返回值的正负零;不能把它当作自然语言排序 |
strcpy、strcat |
复制、追加字符串 | 目标空间必须足够,源与目标不能重叠 |
strncpy |
最多检查源的前 n 项,并按规则补零 | 不保证结果以空字符结尾,可能填满全部 n 项 |
strncat |
最多追加 n 个源字符,最后再补空字符 | n 不是整个目标缓冲区大小 |
strchr、strrchr、strstr |
查字符或子串 | 成功返回内部位置,失败为 NULL |
strspn、strcspn |
计算符合或不符合字符集合的前缀长度 | 集合由字符串参数中的字符构成 |
strpbrk |
找到首次属于给定集合的字符 | 返回原字符串内部位置 |
strtok |
就地切分记号 | 修改输入、保留内部状态、跳过连续分隔符 |
不要向 strtok() 传入字符串字面量。需要同时解析多个序列或保留空字段时,应该重新设计解析过程;strtok_r() 也不能假设是 ISO C11 自带接口。
8.2 按字节处理,不依赖空字符
| 函数 | 作用 | 主要边界 |
|---|---|---|
memcpy |
复制 n 字节 | 两个范围不得重叠 |
memmove |
复制 n 字节 | 允许重叠,仍需保证范围有效 |
memcmp |
比较 n 字节 | 比较对象表示,不等于结构成员的逻辑比较 |
memchr |
在 n 字节内查找 | 不以空字符作为结束条件 |
memset |
逐字节写入指定值 | 不是给每个 int 元素赋同一个整数 |
char text[16] = "abcdef";
memmove(text, text + 2, strlen(text + 2) + 1);
/* text 变成 "cdef",复制范围包含末尾空字符。 */
结构中的填充字节可能不同,因此不能无条件用 memcmp() 判断两个结构是否具有相同的业务内容。全零字节模式也不能在所有 C 实现上保证等同于空指针或浮点零值。
9. 数字转换:验证转换范围与整行内容
| 需求 | 接口 |
|---|---|
| 有符号整数 | strtol、strtoll、strtoimax |
| 无符号整数 | strtoul、strtoull、strtoumax |
| 浮点数 | strtof、strtod、strtold |
转换前把 errno 清零,转换后检查:是否读到数字、是否报告范围错误、末尾是否仍有不允许的内容、数值是否符合业务范围。
atoi() 不能清楚区分合法的零和转换失败,也缺少完善的范围报告,不适合验证外部输入。
下面的完整示例允许首尾空白,要求整条字符串表示一个十进制 int。失败时不修改输出对象。
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
static bool parse_int(const char *text, int *out)
{
char *end;
errno = 0;
long value = strtol(text, &end, 10);
if (end == text || errno == ERANGE || value < INT_MIN || value > INT_MAX) {
return false;
}
while (isspace((unsigned char)*end)) {
++end;
}
if (*end != '\0') {
return false;
}
*out = (int)value;
return true;
}
int main(void)
{
const char *samples[] = {" 42 ", "-17", "12x", "", "999999999999999999999999"};
for (size_t i = 0; i < sizeof samples / sizeof samples[0]; ++i) {
int value = 0;
if (parse_int(samples[i], &value)) {
printf("[%s] -> %d\n", samples[i], value);
} else {
printf("[%s] -> invalid\n", samples[i]);
}
}
return 0;
}
无符号转换函数也可能接受负号,不能仅因为函数名含有 u 就认为负数输入一定被拒绝。若业务不允许负号,应在解析规则中明确拒绝。
errno 不会在每次成功调用后自动变回零,也不是所有标准库函数都通过它报告失败;应按相应接口的规定使用。
10. 动态内存、排序与程序结束
10.1 内存分配
| 接口 | 行为与约定 |
|---|---|
malloc(bytes) |
分配未初始化的空间,失败返回 NULL |
calloc(count, size) |
分配数组空间并把所有字节初始化为零 |
realloc(p, bytes) |
改变块大小,可能移动;非零大小失败时原块仍有效 |
aligned_alloc(alignment, size) |
C11 对齐分配;对齐要求必须受实现支持,大小是对齐值的整数倍 |
free(p) |
释放由相应分配接口取得的块;允许传入 NULL |
/* 已包含 <stdint.h>、<stdlib.h>。
values 为 int *;new_count 为 size_t,且大于零。 */
if (new_count <= SIZE_MAX / sizeof *values) {
int *temp = realloc(values, new_count * sizeof *values);
if (temp != NULL) {
values = temp;
} else {
/* values 仍指向原来的有效块。 */
}
}
统一把零容量作为单独情况处理,不依赖 realloc(p, 0) 执行某一种固定动作。只释放原始分配块的有效入口一次,不能释放数组中间位置、自动变量或字符串字面量。
10.2 排序与查找
qsort() 接收数组、元素数、元素大小和比较函数;bsearch() 在已经按照相同规则排序的数组中查找。
static int compare_int(const void *left, const void *right)
{
int a = *(const int *)left;
int b = *(const int *)right;
return (a > b) - (a < b);
}
不要直接 return a - b;,减法可能溢出。结构排序要明确比较哪些成员;字符串指针数组的比较函数接收的是指向数组元素的指针,需要先取得其中的字符串指针。
标准不保证 qsort() 是稳定排序,也不规定必须使用快速排序算法。bsearch() 遇到重复键时不保证返回第一个匹配元素。
10.3 程序终止
| 接口 | 主要行为 |
|---|---|
exit(status) |
正常终止,执行 atexit 回调并处理标准流清理 |
atexit(func) |
注册无参数、无返回值的退出函数,后注册的先执行 |
quick_exit(status) |
C11 的快速正常终止,执行 at_quick_exit 回调,再终止 |
at_quick_exit(func) |
注册 quick_exit 使用的回调 |
_Exit(status) |
不执行上述退出回调,流清理细节由实现规定 |
abort() |
异常终止,不依赖常规退出清理 |
从最初的 main() 调用返回,相当于以相应状态调用 exit()。可移植地表达成功与失败,使用 EXIT_SUCCESS、EXIT_FAILURE。
_Noreturn 或 <stdnoreturn.h> 中的 noreturn 表示函数不会正常返回给调用者;它既不是 void 的同义词,也不是让编译器自动终止函数。
11. 字符分类、本地化与断言
11.1 ctype.h
常见分类接口有 isalpha、isdigit、isalnum、isspace、isblank、isupper、islower、isprint、ispunct、isxdigit;转换接口有 toupper、tolower。
分类结果是零或非零,不要求“真”一定等于 1。
普通 char 取出的值应先转成 unsigned char,再交给这些接口。getchar() 读出的 int 则应先保留并检查 EOF,不要先转为 char 而丢失结束标记。
int upper = toupper((unsigned char)text[i]);
这些函数受相应本地化规则影响,不是完整的 Unicode 文本处理工具。
11.2 locale.h
程序开始时处于 "C" 本地化环境。setlocale(LC_ALL, "") 尝试采用环境指定的本地化设置,失败时返回 NULL。
| 类别 | 影响范围 |
|---|---|
LC_CTYPE |
字符分类与多字节转换 |
LC_COLLATE |
strcoll、strxfrm 等排序规则 |
LC_NUMERIC |
小数点等数字格式 |
LC_TIME |
日期时间格式 |
LC_MONETARY |
货币格式 |
LC_ALL |
所有支持类别 |
本地化变化可能影响机器可读数据的解析和输出。不要假设所有地区都用点号作为小数点。localeconv() 可以取得格式约定,返回的数据由库管理。
11.3 assert.h
assert(condition) 检查运行时的不变量;定义 NDEBUG 后断言表达式可能完全不求值,因此不能在里面放必要的读写或分配动作。
_Static_assert(condition, "message") 是 C11 的编译期声明,要求整数常量表达式;<assert.h> 还提供 static_assert 宏别名。
外部输入错误应通过实际检查处理,不能只依赖可能被禁用的断言。
12. 数学接口速查
许多数学函数具有 double、float、long double 三组版本,例如 sqrt、sqrtf、sqrtl。三角函数使用弧度。
| 任务 | 常用接口 |
|---|---|
| 三角与反三角 | sin、cos、tan、asin、acos、atan、atan2 |
| 指数与对数 | exp、exp2、log、log2、log10 |
| 接近零时更精细的计算 | expm1、log1p |
| 开方与长度 | sqrt、cbrt、hypot |
| 幂与拆分 | pow、frexp、ldexp、modf、scalbn |
| 取整 | floor、ceil、trunc、round、nearbyint、rint |
| 浮点余数 | fmod、remainder、remquo |
| 绝对值与符号 | fabs、copysign、signbit |
| 分类 | fpclassify、isfinite、isnormal、isinf、isnan |
| 相邻值与组合计算 | nextafter、nexttoward、fma |
| 极值与正差 | fmax、fmin、fdim |
abs() 是 <stdlib.h> 中的整数绝对值接口,不能用来代替浮点的 fabs()。某类型的最小负值若没有对应的正值,直接求其整数绝对值也不能得到可表示结果。
atan2(y, x) 同时利用横纵坐标判定象限,不应简单替换成 atan(y / x)。hypot(x, y) 旨在避免直接平方相加所带来的不必要的中间溢出或下溢。
<tgmath.h> 为相应接口提供按参数类型选择版本的宏。它不意味着任意函数都能接受复数参数,也不能只根据接收结果的变量类型选择精度。
13. 日期与时间
| 接口或类型 | 主要用途 |
|---|---|
time_t、time |
表示和取得日历时间,编码由实现决定 |
difftime(end, start) |
计算两个日历时间的秒数差 |
clock_t、clock |
处理器使用时间,除以 CLOCKS_PER_SEC 转成秒 |
struct tm |
年月日、时分秒等分解时间 |
localtime、gmtime |
转成本地分解时间或 UTC 分解时间 |
mktime |
本地分解时间转回日历时间,并归一化成员 |
strftime |
按格式把分解时间写入缓冲区 |
struct timespec、timespec_get |
C11 提供的秒与纳秒表示及取时接口 |
time_t 不是标准保证的“从某固定年份开始的秒数”。计算日历时间差时使用 difftime(),不要依赖底层编码。
clock() 衡量处理器时间,不等于用户等待的墙上时间。timespec_get(..., TIME_UTC) 取得日历时钟,也不保证单调递增,不能直接当作严格单调的性能计时器。
struct tm 中 tm_year 是从 1900 起算的年数,tm_mon 是 0~11,tm_mday 正常为 1~31,tm_wday 中星期日为 0。
localtime() 和 gmtime() 转换失败时返回 NULL。成功的返回结果可能共享库内的存储,后续调用可能覆盖它;需要保留时先复制结构。
下面的完整示例在 "C" 本地化环境下格式化一个已知日期,不依赖运行时的当前时刻:
#include <stdio.h>
#include <time.h>
int main(void)
{
const struct tm date = {
.tm_year = 2024 - 1900,
.tm_mon = 2 - 1,
.tm_mday = 29,
.tm_hour = 8,
.tm_min = 30,
.tm_sec = 5
};
char buffer[32];
if (strftime(buffer, sizeof buffer, "%Y-%m-%d %H:%M:%S", &date) == 0) {
return 1;
}
puts(buffer);
return 0;
}
输出 2024-02-29 08:30:05。需要星期、年内日期等信息时,应保证对应成员也正确,不能以为只填写年月日就自动推导出所有字段。
14. 知道用途,但不宜套用的接口
14.1 setjmp 与 longjmp
setjmp() 保存可恢复的执行环境,第一次返回 0;有效的 longjmp() 恢复环境后,使它表现为返回非零值。
只能跳回仍然有效的函数调用环境,不能跳回已经返回的函数。调用位置还受标准语法约束,不能随意把 setjmp 藏在任意表达式里。
保存环境后又被修改的、非 volatile 的相关自动局部对象,恢复后可能具有不确定的值。跳转也不会自动清理所跳过路径中的动态资源;它不是 C++ 异常处理或析构机制。
14.2 signal 与 raise
signal() 设置处理方式,raise() 发出信号。SIG_DFL、SIG_IGN 用于请求默认处理或忽略,错误返回值是 SIG_ERR。
异步信号处理函数可执行的工作受到严格限制,不能随意调用 printf、malloc 或 exit。简单设计通常只修改适合信号处理的标志,由正常控制流程完成输出和清理。
volatile sig_atomic_t 用于这种受限制的信号通信场景,不能据此推导出普通多线程访问已经安全。
14.3 原子与线程
<stdatomic.h> 提供原子对象和相应操作;<threads.h> 提供线程、互斥量、条件变量等设施。C11 中应先确认实现是否支持这些可选能力。
原子性不等于任意一组业务操作都自动成为不可分割事务。多个相关字段的一致性、访问顺序和资源生命期仍需要设计;volatile 不能替代原子操作或锁。
14.4 可变参数
va_start 建立遍历,va_arg 读取,va_copy 复制遍历状态,每个相应状态都要用 va_end 结束。
可变参数中的 float 提升为 double,小整数类型发生整数提升。读取时必须使用与实参提升后相匹配的类型;函数无法仅凭省略号自动知道参数的数量与类型。

