清风日常

Stay curious. Stay gentle.

听一会儿

4 首收藏
LOVE 2000 封面

LOVE 2000

遠野ひかる

0:004:23
首页/C Primer Plus/正文

C Primer Plus(第6版)· 附录整理:语法与标准库速查

这份笔记用于写程序时查阅规则、选择接口。内容以 C11 为背景,保留常用知识和关键边界;整数类型、字符编码、浮点计算与 C/C++ 差异另作一份扩展笔记。

文章目录 35 个章节

附录整理:语法与标准库速查

这份笔记用于写程序时查阅规则、选择接口。内容以 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) 理解。

c
/* 错误示例: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 常用括号形式

c
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 清除结束与错误标志,不会自动修复底层错误

正确的读取循环由实际读取结果驱动:

c
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 控制输出容量

c
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 元素赋同一个整数
c
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。失败时不修改输出对象。

c
#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
c
/* 已包含 <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() 在已经按照相同规则排序的数组中查找。

c
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 而丢失结束标记。

c
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" 本地化环境下格式化一个已知日期,不依赖运行时的当前时刻:

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,小整数类型发生整数提升。读取时必须使用与实参提升后相匹配的类型;函数无法仅凭省略号自动知道参数的数量与类型。

Aron 的清风日常

作者 Aron · 发布于 2026.08.27

愿这份记录能为你提供一点参考。发现问题,欢迎通过 GitHub 或 邮件交流。

如果这篇记录对你有用,留下一份喜欢。

← 所有文章继续阅读C Primer Plus(第6版)· 附录整理:整数、字符、数值计算与语言差异 →
SEARCH MY NOTES

在记录里,找一点答案

搜索标题、标签与全文 · 按 Esc 关闭