C 速查 — 简明参考
C(C11/C17)简明自包含参考手册:语法、指针、内存、字符串、结构体与文件 I/O,外加线程、Socket、正则、时间、进程与构建/调试要点。覆盖约 80% 日常场景。
C C17 (ISO/IEC 9899:2018)
ISO C · 过程式 · 手动内存 · 静态 · 弱类型(带强制转换)
建议学习路径
C 新手?按此顺序阅读: 1. Hello World —— 编译、运行、退出码 2. 变量与常量 —— 类型、大小、const 3. 指针与数组 —— &、*、->、指针算术 4. 流程控制 —— if / switch / 循环 5. 函数与可变参数 —— 传值 vs 传指针 6. 字符串与转换 —— printf 格式符表 需要时再查阅文件 I/O 与错误处理。熟练后再看结构体、面向对象风格模式及进阶章节。 有具体任务?使用上方搜索框 —— 试试「malloc」「socket」「pthread」「qsort」或「regex」。
1.Hello World 与构建环境
编译、运行并组织 C 程序:工具链、IDE 选择、多文件、命令行参数、退出码。
最小程序
每个 C 程序都从 main() 开始执行,并返回一个 int 状态给操作系统:0 表示成功,其他值表示出错。最上方的 #include 引入声明,编译器才知道 printf 是什么。
编译与运行(gcc / clang)
C 是编译型语言而非解释型:编译器把源代码翻译成可执行文件,再运行它。始终开启警告(-Wall -Wextra)并固定语言标准(-std=c17),让可移植性问题在编译期就暴露出来。
工具链与 IDE 清单
你需要编译器加编辑器。常见组合:Linux 用 gcc,macOS 用 clang,Windows 用 MinGW-w64 或 MSVC,都可以在 VS Code 或 CLion 里调用。先运行一次版本命令确认工具链可用,再去排查别的问题。
命令行参数
main 可以接收用户在命令行输入的内容:argc 是参数个数,argv[] 是参数本身——argv[0] 永远是程序名。把参数当字符串解析,需要数值时用 strtol/strtod 转换。
多文件
大型程序拆成多个 .c 文件,配一个共享的 .h 头文件放声明。每个 .c 文件单独编译后链接;头文件让声明与定义保持一致,签名一改,所有用到它的地方都会同时暴露。
退出码
main 返回的值会传到 shell:0(EXIT_SUCCESS)表示成功,其他值表示失败。脚本和 CI 都依赖这个约定,所以退出码要有意义。Linux/macOS 用 $? 查看,Windows 用 %ERRORLEVEL%。
环境变量
getenv() 读取进程环境里的变量,返回其值,未设置时返回 NULL。它是不重新编译就注入配置的标准做法——但要把读到的值当作不可信输入。
预处理器基础
预处理器在编译之前运行:#define 定义宏,# 和 ## 连接记号,#ifdef/#if 条件编译代码。它是纯文本、无类型的一层——多数场合优先用真正的函数和 const。
2.变量与常量
声明、类型大小、定宽整数、const 位置、存储类、typedef 与枚举。
基本声明
声明一个变量要先写类型再写名字,并在声明处初始化(C99+)。声明时初始化,避免后面读到未初始化的垃圾值。
类型大小(64 位典型值)
C 只保证最小大小,int 在不同平台上可能是 16 位或 32 位。用 sizeof 打印目标平台的实际大小,保持诚实,绝不硬编码字节数。
定宽类型(C99+,<stdint.h>)
当内存布局必须精确——文件格式、网络协议、哈希——就用 <stdint.h> 里的 int32_t/uint64_t 等。它们只在平台确实提供时才存在,这正是精确宽度名字受青睐的原因。
无符号 / 有符号
无符号类型永不为负,按 2^N 取模回绕;有符号类型可为负。两者混在一个表达式里时,有符号操作数会被提升为无符号——这是经典坑源(见 faq 节)。
const——只读(编译期检查)
const 让变量只读,编译器在构建期就能拒绝意外的写入。凡是不得改变的东西都加上 const:它既说明意图,也利于编译器优化。
const 指针位置(经典易混点)
指针声明里 const 的位置决定保护谁:在 * 之前保护指向的值,在 * 之后保护指针本身。从右往左读类型,就能看懂这两行的差别。
存储类别
存储类别决定变量存在哪里、活多久。auto 是局部变量的默认;static 加在文件作用域上让名字不跨文件可见;static 加在局部变量上让它在多次调用间保值;extern 引用别处定义的名字。
typedef——类型别名
typedef 给类型起新名字。它不是创建新类型,只是别名——但有了别名就能写更短、更一致的类型,把 struct 和函数指针的噪音藏进一个可读的词。
enum——命名常量
enum 给一组相关的整数常量起名字。值从 0 开始逐个加 1,但也可以显式赋值或指定范围。枚举自带文档属性,还能用于 switch。
指定初始化(C99+)
C99 允许按名字初始化成员(.port = 8080)或按下标初始化数组元素([3] = 9),顺序任意。没提到的成员自动清零——比一长串位置初始化清晰得多。
复合字面量(C99+)
复合字面量用类似强转的语法内联构建一个临时 struct/数组值。想给函数传一次性值时,不用先声明具名变量,很方便。
volatile(内存映射 I/O / 信号)
volatile 告诉编译器某个变量可能在程序正常流程之外改变——硬件寄存器、信号处理器或另一个线程。它阻止编译器对访问做缓存或重排。
3.结构体、联合体与聚合类型
结构体布局、联合体、位域、sizeof、_Static_assert 与对齐 —— 真实 C 数据的构建块。
struct——异构记录
struct 把不同类型但互相关联的值归成一个记录。成员按声明顺序存放,中间会插入对齐填充;所以 sizeof(struct) 常常大于成员大小之和。
指向 struct 的指针——用 -> 访问成员
箭头 p->member 是 (*p).member 的简写,对结构体指针解引用访问成员。按指针传结构体很省(一个字),还能让函数修改调用者的记录。
匿名 struct + typedef
可以给没有标签的 struct 加 typedef,一条声明就得到干净的类型名。这是给常用记录类型定义名字的习惯做法,省得处处写 struct。
union——重叠存储(大小 = 最大成员)
union 把多个成员放进同一块内存,大小取最大成员。只有最后写入的那个成员才有效——读其他成员是未定义行为,所以要配一个标签字段。
位域——打包标志位
位域把若干小标志打包进一个整数的比特位,为含大量布尔值的记录省内存。布局由编译器决定,所以位域只留在单个编译单元内,不要跨 ABI 边界。
sizeof——对象占用的字节数
sizeof 给出类型或对象占用的字节数。对仍在作用域内的数组,sizeof a / sizeof a[0] 可得到元素个数——但一旦数组退化为指针,这个技巧就悄悄失效(见 faq)。
_Static_assert(C11+)编译期检查
_Static_assert(cond, msg) 在编译期断言,cond 为假就构建失败。它适合在尺寸和 ABI 假设被破坏的那一刻就校验,而不是等到运行时才发现。
对齐——& _Alignof(C11+)
对齐决定对象可放在内存的哪个地址;编译器给 struct 填充空隙让每个成员自然对齐。_Alignof 报告对象的对齐值,_Alignas 在需要时提高它(SIMD、缓存行共享)。
柔性数组成员(C99+)——struct 尾部数组
struct 可以以一个不定长数组结尾(data[]),长度在分配时决定:一次 malloc 分配 struct 加额外字节。这是运行时才知道缓冲区大小的 C 惯用法。
枚举作为独立类型(C23)
C23 允许固定枚举的底层类型(enum Status : unsigned char),大小不再由编译器决定。否则枚举与 int 兼容,这对多数代码都够用。
4.指针与数组
取地址、解引用、指针算术、数组衰减、指针 vs 数组、const 正确性。
取地址(&)与解引用(*)
& 取对象的地址得到指针;* 反向操作,通过指针读写。两者配合让你能间接访问一个值,并在另一个作用域里修改它。
NULL——空指针
NULL 是“什么都不指向”的指针,不同于任何合法地址。解引用它是未定义行为,通常直接崩溃——所以用之前务必判断是否为 NULL。
指针算术(按 sizeof(*p) 缩放)
指针加整数是按元素前进,不是按字节——p + 2 跳过两个 int。下标 p[i] 其实就是 *(p + i)。越过数组末尾是未定义行为。
数组退化(表达式变指针)
多数表达式中,数组名变成指向首元素的指针。后果是:在函数内部无法从参数恢复数组长度——必须把长度和数组一起传进来。
指针 vs 数组
char 数组自己拥有字节,可修改;指向字符串字面量的 char* 通常指向只读内存。透过字面量指针写入是未定义行为,常常崩溃。
多级指针
指向指针的指针(int**)多了一层间接。它用于字符串数组、二维结构,以及“由函数分配内存、调用者负责释放”的输出参数。
函数指针
函数指针存的是函数的地址,可以间接调用。它是回调、分派表,以及 oop 节里那些面向对象模式的基石。
指针数组(如 argv)
char* 数组是存放字符串列表的惯用容器,argv 就是一个。每个元素指向各自以 NUL 结尾的字符串,数组本身只是一串指针。
const 正确性
把指针参数声明为 const——例如 const char* s——等于承诺函数不会改动指向的内容。调用者就能放心传字符串字面量和数组,编译器会强制这份承诺。
数组指针 vs 指针数组
int *a[5] 是 5 个指针组成的数组;int (*b)[5] 是指向 5 个 int 数组的指针。括号改变一切。复杂声明从右往左读,或者用 cdecl 解码。
5.流程控制
if/else、三元、switch、for、while、do-while、break/continue、goto 与 setjmp/longjmp。
if / else if / else
按条件分支:第一个条件为真的分支执行,否则走 else。else-if 链按顺序测试多个备选——命中第一个即停。
三目运算符(表达式)
cond ? a : b 是表达式而非语句:按 cond 取 a 或 b。适合紧凑的取值选择;如果一行放不下、读不清,就改用 if/else。
switch——不 break 就穿透
switch 按整数值分派。除非 break,执行会穿透进下一个 case——要么有意利用穿透(空 case 堆叠),要么每个 case 都 break。default 处理未匹配的值。
for——在 init 中声明计数器(C99+)
for(init; cond; step) 是标准计数循环。在 init 部分声明计数器(C99+)能把它限定在循环内,避免和外层同名变量冲突。
while / do-while
while 每次迭代前测试条件,可能一次都不执行。do-while 总是先执行一次再测试——适合菜单和校验提示这类“至少问一次”的场景。
break / continue
break 立即退出最内层循环或 switch;continue 跳到下一次迭代。用好了能摊平嵌套逻辑、去掉标志变量;用坏了会隐藏控制流,所以尽量少层。
goto——跳出深层嵌套(少用、慎用)
goto 跳到某行标签。它合理的用途是跳出深层嵌套、集中做错误清理——即 goto-cleanup 惯用法。除此之外,return 或标志变量几乎总是更清晰。
setjmp / longjmp 非局部跳转
setjmp 保存执行上下文,longjmp 从任意位置恢复它,一路回卷到保存点。途中所有清理都被跳过——没有析构函数会执行——所以比起 goto 或显式 return,通常不划算。
循环惯用法
值得认识的循环形态:对计数范围的下标循环、对数组的指针遍历、以及带 break 的无限循环。选最能直接表达意图的那种。
逻辑短路求值
&& 和 || 从左到右求值,结果一确定就停止。这就是 ptr && ptr->x 安全的原因:第二个操作数只在第一个为真时才执行,所以可以在行内守卫解引用。
6.函数与可变参数
传值 vs 传指针、函数指针与 typedef、可变参数、递归与 _Generic。
按值传递——调用者的变量不变
C 所有参数都按值传递——被调函数拿到的是副本,修改它不影响调用者。简单的标量和小的结构体这样传;需要修改或担心大对象时用指针形式。
按指针传递(修改调用者)
要修改调用者的变量,就传它的地址。被调函数解引用指针并写入,改动就对调用者可见——scanf 这类函数正是这样产生输出的。
const 参数——只读契约
const 参数如 const char* s 是只读契约:函数承诺不修改其指向的内容。它让调用者能安全传字符串字面量,也让 API 意图一目了然。
数组参数退化(需传长度)
数组参数会退化为指针,长度不会跟着传过去。务必显式传长度——否则函数无从知道数据在哪里结束。
函数指针 + typedef
给函数指针签名加一次 typedef,之后用别名声明变量和参数。这样分派表和回调读起来不再是一堵括号加星号的墙。
可变参数——printf 风格函数
签名带 ... 的函数接收可变数量的参数,如 printf。用 va_start/va_arg/va_end 读取;固定的参数必须说明后面还有多少,否则调用无法被安全解码。
可变参数转发——v* 系列函数
要把可变参数转发给另一个可变参数函数(比如日志封装转发给 vfprintf),用 v* 系列并把 va_list 本身传过去。再用 va_arg 重读会破坏列表。
递归
递归函数在更小的同类问题上调用自身,直到触及基线。每次调用都占栈空间,所以深度大或不可控时优先用迭代。
_Generic(C11+)——编译期类型分派
_Generic(C11)根据控制表达式的类型,在编译期选出结果表达式——一种轻量模拟函数重载、写类型安全宏的办法。
noreturn(C11+)与 inline
noreturn 标记永不返回的函数(如致命错误例程),编译器因此能推断不可达代码。inline 只是请求而非保证——编译器仍可能发出真实的调用。
7.字符串与数值转换
NUL 结尾字符串、安全写入、strtol/strtok/sscanf 转换与 printf 格式符表。
基础——以 NUL 结尾的字符数组
C 字符串是以 NUL 字节('\0')结尾的字符数组。strlen 数到终止符为止;任何装字符串的缓冲区都得给它留位置,否则写入就溢出。
复制 / 比较 / 拼接(不安全 vs 安全)
strcpy/strcat 不检查边界,源串太大时会静默溢出。优先用 snprintf 做有界、总是以 NUL 结尾的写入;strcmp/strncmp 比较;strncat 最多复制 n 个字符再加终止符。
查找
strchr 找字符首次出现,strrchr 找最后一次,strstr 找子串首次出现。都返回指向字符串内部的指针,找不到返回 NULL。
mem*——原始字节(可处理任意内存,含 NUL)
mem* 函数(memcpy、memmove、memset、memcmp)处理原始字节,能应对内嵌的 NUL。memcpy 和 memmove 的差别在于是否容忍重叠——可能重叠时用 memmove。
数值转换:strtol/strtod
用 strtol/strtod 解析数值:它们会报错并指出解析停在哪。atoi 遇到垃圾静默返回 0,会掩盖非法输入——追求正确的代码别用它。
健壮解析——校验整个字符串
一次完整的解析要校验三件事:strtol 后 errno 未置位、至少消耗了一位数字(end != s)、整个字符串都被读完(*end == '\0')。跳过任何一条,尾部垃圾就溜进来了。
分词:strtok_r 可重入
strtok 会原地修改字符串,在每个分隔符处插入 '\0';带 _r 后缀的版本可重入(线程安全)。还需要原字符串的话,先复制一份。
sscanf——用格式串解析(返回匹配数)
sscanf 用格式串解析字符串,返回成功匹配的字段数。检查这个返回值:比预期低,说明输入不是你以为的形状。
printf 格式表
printf 家族共用一套格式语言:%d int、%ld long、%zu size_t、%f double、%s 字符串、%p 指针,加上宽度和精度修饰符。格式符必须与实参类型精确匹配——不匹配是未定义行为。
安全地拼接字符串
snprintf 最多写 n 字节且总是以 NUL 结尾,是安全拼字符串的办法。对不断增长的文字,记住长度并 realloc 缓冲区,或用一个小型动态缓冲辅助函数。
8.数组、排序与查找
数组遍历、带比较器的 qsort/bsearch、二维数组与结构体数组。
定长数组——带长度迭代
C 数组是一块固定大小的连续元素。用长度迭代:真正的数组仍在作用域内时 sizeof a / sizeof *a 有效,但一旦传给函数,大小就丢了。
初始化模式
数组可按位置、按指定下标([2] = 9)、或整体清零({0})初始化。二维数组是数组的数组,行主序存放:m[i][j] 在每行内连续。
qsort——用比较器排序
qsort 用比较器原地排序,比较器返回 <0、0 或 >0。比较器收到的是指向元素的指针,所以先把 void* 参数转回元素类型再安全比较(避免溢出)。
bsearch——在已排序数组上二分查找
bsearch 做二分查找——但只对已经排序的数组。它返回指向匹配元素的指针,找不到返回 NULL。大数组上先排序再查找,胜过线性扫描。
字符串比较器(char* 数组)
给 char* 数组排序必须比较字符串而不是指针值。比较器转成 char* const*,解引用拿到每个字符串指针,再调用 strcmp。
结构体数组 + 按字段排序
要按某个字段给结构体数组排序,写一个接收两个结构体指针、比较该字段的比较器(比如 a->key 对 b->key 三路比较)。qsort 就会重排整个记录。
数组指针参数(保持二维形状)
把二维数组传给函数,参数类型里必须带列数——int m[][COLS]——编译器才能算出每行起点。第一维(行数)可以自由变化。
动态数组模式(另见 mem 节)
可增长集合是 malloc 出来的块,满了就 realloc:容量和长度分开记,增长时翻倍,用完 free。这是高层语言里 vector/list 在 C 中的对应物。
变长数组(C99 可选)
变长数组按运行时值在栈上分配(int vla[n])。C11 中它是可选的,n 太大时可能撑爆栈——大的或需要可移植的东西优先用 malloc。
二维数组——行主序
int m[ROWS][COLS] 是连续的行主序块;m[i][j] 就是 *(*(m + i) + j)。下标需要知道行长才能算出行起点,所以形状必须在编译期确定。
9.动态内存与所有权
malloc/calloc/realloc/free、栈 vs 堆、对齐、所有权规则与 goto cleanup 模式。
分配与释放
malloc 返回未初始化的堆内存,每块分配最终都要释放。用前务必检查是否为 NULL;释放后把指针置 NULL,之后的重复释放就成了安全的空操作。
calloc:count×size 清零
calloc(count, size) 会清零内存,并替你相乘两个参数,还能检测溢出。需要已初始化存储且元素个数已知时优先用它。
realloc——调整大小;可能移动内存块
realloc 调整分配的大小,可能把块搬到新地址。先用临时变量接收返回值并检查:失败时旧块仍然有效,但如果你先覆盖了指针,它就泄漏了。
栈 vs 堆
栈内存自动管理、快、返回时回收,但小且短命。堆内存手动管理、大、能活过函数。又大又长寿的缓冲区该放在堆上。
所有权——一组简单规则
一组最少的所有权纪律能让内存问题可控:谁分配谁释放;每个指针只释放一次;释放后置 NULL;realloc 失败时保留旧指针。约定就是 C 唯一的资源管理器。
堆上的字符串
堆上字符串需要 strlen(s) + 1 字节——多出来的是 NUL 终止符。malloc(strlen(s) + 1) 再复制;忘了 +1 就是一个待引爆的一字节溢出。
含内部指针的 struct
拥有堆数据的 struct 有两块分配要释放:先内层缓冲区,再 struct 本身。决定并写明谁拥有内部指针,按分配的逆序释放。
goto cleanup(单一出口)
C 没有析构函数,所以 goto-cleanup 就是 C 的 RAII:每个函数一个出口、一个清理块,所有路径都到达它。每个资源在每条路径上恰好释放一次。
对齐 / 超对齐(C11)
aligned_alloc 能按指定对齐申请内存(SIMD、缓存行共享),大小必须是对齐值的整数倍。普通 malloc 只保证平台的默认对齐。
内存泄漏 / 溢出检测工具(见 build 节)
泄漏、溢出和释放后使用都藏到进程死掉才暴露——用 valgrind 或 AddressSanitizer 机械地找出来。两者都在 build 节讲过,是你能加的最便宜的安全网。
10.面向对象风格模式
C 是过程式语言,但结构体 + 函数指针可模拟类、继承与多态。
“类” = struct + vtable
C 模拟类:用 struct 装数据,用一张函数指针表(vtable)装行为。方法就是带显式 self/this 指针参数的普通函数。
通过内嵌实现继承
把基类 struct 内嵌为第一个成员就得到继承:派生 struct 的地址等于基类的地址,所以派生值可以在任何期望基类的地方使用,基类方法原样生效。
构造函数 / 析构函数配对
配一个负责分配并初始化的 init 函数和一个负责释放并回收的 free 函数——C 版的构造/析构函数。每个被创建的对象都必须恰好销毁一次。
不透明指针——隐藏实现(PIMPL)
头文件里只前置声明 struct,定义只放在 .c 文件里。调用者能持有指针但看不到内部——这就是 PIMPL,隐藏实现,让内部改动不破坏调用者。
接口 = 函数指针组成的 struct
装满函数指针的 struct 就是接口:任何填充它的实现都能插进来,每个函数再收一个 void* self 作为实例上下文。这就是 C 形态的多态。
类型擦除数据(void *)
void* 抹掉类型,让一个容器能装异构的值。想安全地还原类型,通常得加一个标签字段记录指针真正指向什么。
通过返回 *this 实现方法链
每个 setter 返回对象本身(*this),调用就能连起来:builder->setA(x)->setB(y)。它让 builder 风格配置变得紧凑,代价是返回值可以被忽略。
单例(懒加载,线程不安全)
懒加载单例在首次使用时,把实例分配进一个 static 指针。这个简单版本不是线程安全的——两个线程可能竞争首次分配——一旦被共享就得加锁。
引用计数(简单版)
retain/release 给对象配一个引用计数,降到零就释放。它是 Objective-C 的基础,对 C 对象而言是比垃圾回收更轻的所有权模型。
完整 OOP 可行(glib)
C 里存在完整的 OOP 框架——glib 和 GObject 就是证明——但它们带来真实复杂度。最简的 vtable 加不透明指针就能覆盖多数需求;重武器留给真正需要的项目。
11.错误处理
无异常:errno、返回码 + out 参数、goto cleanup、错误枚举与断言。
调用失败后的 errno
库函数和系统函数失败时设置全局 errno,你必须在调用后立刻读取或复制——任何后续调用都可能覆盖它。strerror(errno) 把错误码转成可读信息。
返回码 + 出参(常见 C 惯用法)
C 主流错误惯用法:成功返回 0,失败返回负错误码,结果通过出参指针写出。失败时结果指针保持 NULL/不动,调用者可以据此判断。
错误枚举——显式错误分类
枚举你的函数可能产生的错误种类——OK、INVALID_INPUT、NOMEM、IO——并返回它们。具名错误码自带文档、适合 switch,还逼你想清楚每种失败模式。
assert——调试用(NDEBUG 移除)
assert(cond) 在 cond 为假时中止程序,开发期抓 bug。它在 -DNDEBUG 下被编译掉,所以绝不能守卫用户依赖的行为——只校验你假设为真的不变式。
perror——打印 errno 描述
perror 把你的消息连同当前 errno 的描述打印到 stderr 一行。这是报告系统调用失败原因的最快方式——errno 部分不用自己写格式串。
_Static_assert——编译期
_Static_assert 在编译期而不是运行期检查假设。用它校验尺寸、偏移和 ABI 约束,避免它们在生产二进制里静默失败。
线程安全的 strerror_r(POSIX)
strerror 不是线程安全的,会覆写自己的内部缓冲区。strerror_r(POSIX)改写入调用者提供的缓冲区,并发线程各拿各的消息。
检查系统调用返回值
系统调用会失败——文件、套接字、内存都会。忽略返回值会把一次细微失败变成稍后的崩溃或数据损坏;fread/fwrite 即使成功也可能返回短计数,所以务必检查。
用 ferror/feof 区分错误与文件末尾
读循环结束时,ferror() 和 feof() 告诉你原因:是真正的 I/O 错误还是到了文件末尾。EOF 是正常状态而非错误——用 ferror 区分两者。
12.文件 I/O
stdio 流、逐行与整文件读取、二进制 I/O、POSIX 描述符、mmap 与目录遍历。
打开 / 关闭
fopen 返回 FILE* 流,失败返回 NULL;fclose 冲刷并释放它。模式是 r/w/a 加 + 表示读写、b 表示二进制。打开失败后绝不要继续用那个指针。
读取整个文件(小文件)
小文件的做法:seek 到结尾、ftell 拿到大小、rewind、malloc size+1、再 fread 读全。确认读到的字节数与请求一致——调用之间文件可能变了。
逐行读取(惯用法)
fgets 安全地读一行到固定缓冲区,停在换行符或缓冲区上限。用 strcspn 去掉结尾换行——它会被保留在缓冲区里。这是逐行处理文本的惯用方式。
写入
fprintf/fputs/fputc 分别向流写格式化、字符串和字符输出。错误既出现在写入时也可能在 fclose 时,所以要检查 fclose 的结果,防止失败的冲刷被漏掉。
先格式化到字符串(安全)
先用 snprintf 把整段输出拼进缓冲区,再一次写出去。这样能避免并发来源的交错写入、写前可校验,实际也保持写入原子性。
二进制 I/O——读写原始字节
fread/fwrite 搬运原始字节,最适合定长记录:恰好读 sizeof(record) 并处理返回值。短读不是错误就是 EOF——用 ferror/feof 区分。
POSIX 文件描述符
stdio 之下,Linux/macOS 提供对 int 文件描述符的 open/read/write/close:控制精细、无缓冲、坑也更多。要跨界就用 fdopen(fd 换 FILE*)或 fileno(FILE* 换 fd)。
mmap——把文件映射进内存
mmap 把文件直接映射进地址空间,读写变成普通内存访问——大文件很快。munmap 释放它;文件不能为空,映射大小按页对齐。
目录遍历
opendir/readdir/closedir 逐个列出目录项。跳过隐含的 '.' 和 '..' 条目,再用 stat 查每个名字的文件类型和大小。
stat——文件元数据
stat/lstat/fstat 返回文件的元数据——大小、修改时间、权限、类型。tmpfile() 给你一个匿名临时文件,关闭时自动删除,适合存临时数据。
13.常见坑
十个经典 C 坑 —— 正确写法标 GOOD,错误写法标 BAD。
缓冲区溢出
写过一个固定缓冲区的末尾会破坏相邻内存,是顶级安全漏洞。给每处写入加上限:strncpy 手补 NUL、snprintf、或带长度的 fgets。Sanitizer 能抓到人眼漏掉的问题。
释放后使用 / 双重释放
释放后使用是未定义行为,往往可被利用;同一块内存释放两次会破坏分配器。free 后立刻把指针置 NULL——free(NULL) 是安全的空操作。
内存泄漏
每块分配都必须在每条路径上释放;泄漏静默膨胀,直到进程死亡。AddressSanitizer 和 valgrind 默认就会报告——开发期运行,而不是崩溃之后。
= 与 ==
单个 = 是赋值;== 是比较。if (x = 0) 会赋零且永远为假——一个无声又难发现的 bug。用 -Wall 编译,条件里的赋值要加括号。
有符号溢出是 UB
有符号整数溢出是未定义行为:编译器可能假定它永远不会发生并据此优化。加大的有符号值之前先扩成 long long,或者就用你本意的无符号回绕。
未初始化变量
读未初始化的局部变量是未定义行为,通常拿到垃圾值。在声明处初始化每个变量——编译器 -Wuninitialized 才有真东西可查。
sizeof 中的数组退化
函数内部的数组参数是指针,sizeof 得到 8(指针大小)而非数组大小。sizeof a / sizeof a[0] 只在真正的数组仍在作用域内时有效——显式传长度。
差一错误
循环边界是经典的差一错误:n 个元素要迭代 i < n(下标 0..n-1),而不是 i <= n 那样多碰一个越界。大小 n 的数组最后一个合法下标是 n-1。
printf 格式不匹配
格式符与实参类型不匹配是未定义行为——用 %s 配 int 可能直接崩溃。size_t 用 %zu,%d/%ld/%lld 要对上类型,开 -Wformat 抓不匹配。
有符号 / 无符号比较
有符号与无符号比较时,有符号操作数被提升为无符号,负数变成巨大的正数。把两侧都显式转成同一有符号类型,或者干脆别混用。
14.线程与并发
pthread create/join、互斥锁、条件变量、原子操作与 C11 线程 —— 构建与链接加 -pthread。
构建与头文件
线程程序用 -pthread 编译,它链接 pthread 库。包含 <pthread.h>,并把每个 pthread_* 的返回值当成要检查的错误——这些 API 不设置 errno。
创建与等待
pthread_create 启动一个线程运行某个函数,该函数接收一个 void* 参数;pthread_join 阻塞等待它结束。传入的参数和传回的 void* 就是跨线程传递数据的通道。
通过 void* 传出数据
线程把结果以 void* 返回——通常是指向 malloc 出来的值的指针。调用者 join 后把返回的指针强转回来、读出值、再释放。
互斥锁——保护共享状态
互斥锁串行化对共享状态的访问:访问前加锁,之后解锁。临界区保持短小——锁是瓶颈,长时间持有还会引来死锁。
条件变量——等待 / 通知
条件变量让线程睡下,直到另一个线程通知某个谓词改变了。永远在 while 循环里等待(有虚假唤醒),并且要持有互斥锁,醒来后重新检查谓词。
分离线程——无需 join
分离线程结束时会自行清理,无需 join。只在确定永远不会等它的时候才分离——也绝不要把指向调用者栈的指针传给分离线程,那时栈可能已经没了。
C11 线程(可移植)
C11 的 <threads.h>(thrd_t)是比 pthreads 更薄、可移植的替代——能力更弱,常常只是对 OS API 的薄封装。POSIX 上做正经事,pthreads 仍是标准选择。
原子操作(C11)——<stdatomic.h>
atomic_int 和 atomic_fetch_add 提供有定义内存顺序的无锁加载、存储和自增——比互斥锁快的简单共享计数器,也比普通 int 更正确(不会竞争)。
线程局部存储(C11)
_Thread_local(C11,GCC 里是 __thread)让每个线程拥有自己的变量副本。它适合每线程缓存,也适合不得互相覆盖的错误槽。
线程中的错误处理
pthread 函数返回 errno 风格的错误码而不是设置 errno。把返回值接住并用 strerror(rc) 格式化诊断——不检查的失败通常表现为神秘的挂起或崩溃。
死锁规避
死锁来自“持有所需之物还等待别处所需”。按固定的全局顺序加锁、让临界区尽量小、优先一资源一锁,需要有界等待时用带超时的锁。
15.网络(Socket)
getaddrinfo、TCP 客户端与服务端、最小 HTTP 请求 —— Linux/macOS 下无需额外库链接。
构建与头文件
POSIX 套接字需要 <sys/socket.h>、<netinet/in.h> 和 <arpa/inet.h>;Linux/macOS 无需额外链接库。用 getaddrinfo 解析地址,不要手填 sockaddr 结构。
TCP 客户端——解析、连接、收发
TCP 客户端用 getaddrinfo 解析服务器、创建套接字、再 connect。地址列表可能很长——逐个尝试,用第一个成功的,失败的随手关掉。
发送 / 接收
send/recv 在已连接的套接字上搬运字节,但都不保证一次搬完。循环直到收满整条消息或遇到 EOF,一路跟踪还剩多少字节。
TCP 服务器——socket 流程
服务器创建套接字、设 SO_REUSEADDR、bind 到端口、listen,再循环 accept 连接。每次 accept 返回一个专属于该客户端的新套接字——监听套接字从不被读。
解析主机名 -> IP
getaddrinfo 把主机名和端口变成一列候选地址,顺带处理 IPv4/IPv6 和服务名。逐个试到成功为止,然后用 freeaddrinfo 释放。
阻塞 vs 非阻塞
套接字默认阻塞:recv 一直等到数据来。O_NONBLOCK 让调用在没准备好时返回 EAGAIN,poll/epoll/select 就能在单线程里监管大量套接字。
UDP——sendto/recvfrom(无连接)
UDP 无连接:sendto/recvfrom 每次都要带上目标地址,没有握手、没有顺序、没有投递保证。适合对延迟敏感而容忍丢失的流量。
字节序——htons/htonl 与网络序
网络字节序是大端;宿主机可能是小端。填 sockaddr 字段、以及从线路上读数字协议字段时,用 htons/htonl/ntohs/ntohl 转换。
Windows(Winsock)的区别:
Windows 用 winsock2.h,使用套接字前要先调一次 WSAStartup,用 closesocket() 而不是 close() 关套接字。其余 API 与 POSIX 套接字一致,代码大体可移植。
错误码
套接字调用失败返回 -1 并置 errno——ECONNREFUSED、ETIMEDOUT、ECONNRESET——getaddrinfo 则返回自己的错误码给 gai_strerror。每一步都检查;这里的静默失败会变成之后的挂起。
HTTPS 需要 TLS 库
裸套接字只给你 TCP。加密需要 TLS 库——OpenSSL 的话先建 SSL_CTX,用 SSL_new/SSL_connect 包住套接字,再用 SSL_read/SSL_write——或者用 libcurl 走高层 HTTP+TLS API。
16.时间与日期
墙上时钟时间、UTC/本地转换、strftime 格式化与高精度单调计时。
Unix 纪元秒
time(NULL) 返回自 1970-01-01 00:00 UTC 以来的整秒数(time_t)——存储时间戳用的无时区、普适的瞬间。
转换为 UTC / 本地 struct tm
gmtime 和 localtime 把 time_t 拆成 struct tm(年、月、日、时……)——分别是 UTC 和本地时区。注意偏移:tm_year 是自 1900 起的年数,tm_mon 是 0-11。
用 strftime 格式化
strftime 用 %Y %m %d %H:%M:%S 等把 struct tm 格式化成可读文本——日期界的 printf。它是拼日志行和人类可读时间戳的标准方式。
把日期解析回 time_t
strptime(POSIX)把文本解析进 struct tm,mktime 再转成 time_t。把 tm_isdst 设为 -1,让 mktime 自己处理夏令时而不是猜错。
已过墙钟时间
difftime(b, a) 返回两个 time_t 之间的墙钟秒数。它简单,对粗粒度时长够用,但墙钟会跳——做测量要用单调时钟。
高精度单调时钟
clock_gettime(CLOCK_MONOTONIC) 测量的流逝时间不受 NTP 和夏令时调校影响——基准测试、超时、任何不许跳的计时都用它。
纳秒级睡眠(C11 <time.h>)
nanosleep 以 timespec 精度(纳秒级)挂起线程。被信号打断时会报告剩余时间,好让调用继续睡完。
本进程消耗的 CPU 时间
clock() 返回本进程消耗的 CPU 时间,不是墙钟时间。对比 CPU 与墙钟时间,能看出任务在等 I/O 而非在计算。
获取时区偏移
tm_gmtoff(GNU/glibc)保存本地时间相对 UTC 的秒数(东为正),让你不用猜就能渲染 RFC 822 风格的 +0800 偏移。
不用 strftime 输出 ISO 8601
用 snprintf 按 UTC struct tm 的字段拼出 ISO 8601 时间戳。输出与语言环境无关、无歧义——接口和日志的理想选择。
17.进程与信号
fork、exec、wait、进程间管道、信号处理与最小守护进程。
平台与头文件
fork/exec/wait 和信号是 POSIX(Linux/macOS);Windows 用 CreateProcess,模型不同。本节调用需要包含 <unistd.h>、<sys/wait.h> 和 <signal.h>。
fork——克隆当前进程
fork 克隆当前进程。两份副本从同一返回点继续:子进程看到 pid 0,父进程看到子进程的 pid。子进程应当 _exit()——从 main 返回会跑父进程的清理并重复冲刷缓冲区。
exec——替换进程映像
exec 用另一个程序替换当前进程映像:pid 不变,代码全新。它只在失败时返回(随后调用 _exit(127),shell 惯例)。环境、文件描述符和工作目录都会保留。
fork/exec/wait 运行命令
运行外部命令就是 shell 自己的那套:fork(克隆)、execvp(换映像)、waitpid(回收)。WEXITSTATUS 取出子进程的退出码,WIFSIGNALED 告诉你它是否被信号杀死。
管道——子进程写、父进程读
pipe(fd) 创建一对相连的描述符:一端写、一端读。fork 之后各进程关掉自己不用的那一端,读端在写端关闭时就看到 EOF。
信号处理器
signal() 为异步信号注册处理器。处理器内部只能调用 async-signal-safe 函数——write、_exit、signal、kill——绝不能 printf 或 malloc,它们不可重入。
sigaction——健壮的 API
sigaction 是 signal() 的健壮替代:能在处理器运行期间屏蔽其他信号、设置 SA_RESTART 恢复被中断的系统调用、并让处理器知道是哪个信号触发的。新代码优先用它。
临时屏蔽信号
sigprocmask 在临界区周围临时推迟信号、之后再解除,处理器就不会在共享状态改了一半时插入。配合一个处理“保存下来的事件”的处理器,信号用法才安全。
杀死进程 / 检查存在性
kill(pid, sig) 给进程发信号;kill(pid, 0) 只检查进程存在且你有权发信号,不真的送达。负数 pid 针对整个进程组。
僵尸进程——回收子进程
先于父进程 wait 就退出的子进程会变成僵尸,占着一个 pid 直到被 wait/waitpid 回收。忽略 SIGCHLD 让子进程自动被回收,或在处理器里显式回收。
守护进程骨架(双重 fork)
守护进程脱离控制终端:先 fork 让子进程不是进程组组长,再 setsid 开启新会话,再 fork 一次,chdir('/'),并把 stdio 重定向到 /dev/null 或日志。
18.POSIX 正则表达式
regcomp/regexec/regerror:编译模式、匹配与提取捕获组、遍历所有匹配。
设置与头文件
POSIX 正则(regcomp/regexec)内建于 Linux/macOS 的 libc,无需额外库。传 REG_EXTENDED 时使用 ERE 语法——默认的 BRE 是另一套更老的方言。
编译与匹配
先用 regcomp 编译一次模式(并检查返回值!),再用 regexec 对字符串匹配。用 regfree 释放编译好的模式——每个 regcomp 都配一个 regfree。
捕获组
括号分组会落入 regmatch_t 条目:m[1] 保存第 1 组在匹配文本中的起止偏移。用 %.*s 精度技巧配合偏移对打印捕获片段。
遍历所有匹配(用 REG_NOTBOL 循环)
要找全部匹配,就在字符串尾部循环 regexec,每次越过一个匹配。第一次迭代之后传 REG_NOTBOL,避免 ^ 在字符串中途重新锚定。
常用标志
REG_EXTENDED 开启 ERE 语法(用它);REG_ICASE 大小写不敏感匹配;REG_NOSUB 跳过捕获跟踪换取提速;REG_NEWLINE 让 ^/$ 匹配行首行尾而不是整个字符串。
ERE 语法速查
ERE 速查:. 任意字符,^ $ 锚点,[] 字符类,* + ? 重复,{m,n} 有界重复,(a|b) 交替,反向引用,以及括号内的 POSIX 类如 [[:alpha:]]。
替换——没有内建;手动拼输出
POSIX 正则没有内建替换。拼输出:循环匹配,把匹配之间的文本复制进来,在每个匹配处插入替换串——和遍历全部匹配的循环是同一个套路。
转义用户输入
要把用户输入嵌进模式里,先给每个元字符([\\^$.|?*+()[])加上反斜杠转义。否则精心构造的输入会改变你模式的含义——这是注入漏洞,不只是逻辑问题。
检查 regcomp 并 regfree
regcomp 遇到坏模式会失败——务必检查返回值并用 regerror 格式化。而且要每个 regcomp 配一个 regfree,否则循环里反复匹配会泄漏编译好的模式。
局限与性能
复用编译好的 regex_t:每匹配一次都编译是浪费。POSIX 正则会回溯,病态模式在不可信输入上可能很慢——重度或对抗性使用,优先 PCRE2 或 RE2。
编译期匹配,实现 switch 式分派
启动时预编译若干模式,按顺序链式调用 regexec。当输入可能匹配多种形状时,它读起来像一张清晰的路由表——比一墙字符串比较强。
19.构建与调试
编译器标志、最小 Makefile、gdb、消毒器与 valgrind —— 找出 FAQ 章节所警告 bug 的工具。
常用 gcc/clang 标志
-std=c17 固定语言,-Wall -Wextra 开启有用警告(一直开),-g 加调试信息,-O0/-O2 选优化级别,-fsanitize=address,undefined 加运行时检查。零成本还能抓到真 bug。
Makefile——极简构建驱动
极简 Makefile 把编译器、标志、目标名和对象列表放进变量,依靠隐式规则 %.o:%.c,再提供一个 clean 目标。make 只重建有变化的部分——这正是它的意义。
调试构建 + 发布构建
调试构建用 -g -O0 保留完整符号、不优化;发布构建用 -O2 -DNDEBUG,顺带把 assert 编译掉。两个目标都留着,才能在用户跑的确切配置下复现崩溃。
gdb 基础
gdb ./prog,然后:break main、run、next/step 逐行、print x 查看、backtrace 看调用栈、continue、quit。配合 -batch -ex 还能非交互地给 CI 生成崩溃转储。
AddressSanitizer(ASan)
编译时加 -fsanitize=address,undefined,程序就会在运行时带着栈回溯精确报告溢出、释放后使用和泄漏。比 valgrind 快也清晰——第一个要上的工具。
valgrind 内存检测
valgrind --leak-check=full ./prog 无需重编译就能报告非法读写、释放后使用和确定泄漏——但慢约 20-50 倍。在无法重编译或需要更多细节时使用。
UBSan 标志
-fsanitize=undefined 在运行时用精确报告抓有符号溢出、未对齐访问等未定义行为。和 -fsanitize=address 组合,一次构建同时覆盖内存与 UB。
编译到汇编 / 预处理
gcc -S 输出汇编,gcc -E 显示预处理后的源码,gcc -c 生成不链接的目标文件。每个都是一扇看编译器与预处理器到底做了什么的小窗。
静态分析(可选)
gcc -fanalyzer、clang --analyze 和 cppcheck 不用运行程序就能找 bug——空指针解引用、泄漏、未初始化使用。加进 CI 很便宜,还能抓到测试漏掉的错误类别。
构建系统
Make 适合中小项目;CMake 生成 Make 或 Ninja 文件,跨平台可移植;Meson 更新也更快。单个文件的话,一个 Makefile 或 shell 别名就够。
关于本速查
本页是 ISO C(C11/C17)的自包含速查手册,覆盖语言本身和标准库在系统编程中约 80% 的常见用法,外加真实项目中常用的并发、Socket、正则与构建工具。权威参考见 C11 标准(ISO/IEC 9899:2011)与 cppreference 的 C 章节。 19 个章节各自聚焦一个主题 —— 从你的第一个程序、指针,到字符串、内存所有权与常见坑。每章拆分为 6–10 个子主题,附 5–15 行简洁示例。代码块刻意短小自明,每块都有复制按钮,可直接粘贴到编译器。 全部内容在你的浏览器内运行 —— 不上传、不追踪。本页是 GuruToolkit 免费开发者工具集的一部分;这里的代码片段可自由使用,不提供任何担保。
版本 2.3.0