本工具使用的开源库

本工具代码中捆绑了 1 个开源库。

C 速查 — 简明参考

C(C11/C17)简明自包含参考手册:语法、指针、内存、字符串、结构体与文件 I/O,外加线程、Socket、正则、时间、进程与构建/调试要点。覆盖约 80% 日常场景。

C

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 是什么。

1
2
3
4
5
6
#include <stdio.h>
int main(void) {
printf("Hello, world!\n");
return 0; // 0 = success to the OS
}

编译与运行(gcc / clang)

C 是编译型语言而非解释型:编译器把源代码翻译成可执行文件,再运行它。始终开启警告(-Wall -Wextra)并固定语言标准(-std=c17),让可移植性问题在编译期就暴露出来。

1
2
3
4
5
6
7
8
9
// $ gcc hello.c -o hello
// $ ./hello
//
// With warnings + standard version:
// $ gcc -std=c17 -Wall -Wextra -pedantic hello.c -o hello
// $ clang -std=c17 -Wall -Wextra hello.c -o hello // macOS
//
// Windows (MinGW):
// $ gcc hello.c -o hello.exe && hello.exe

工具链与 IDE 清单

你需要编译器加编辑器。常见组合:Linux 用 gcc,macOS 用 clang,Windows 用 MinGW-w64 或 MSVC,都可以在 VS Code 或 CLion 里调用。先运行一次版本命令确认工具链可用,再去排查别的问题。

1
2
3
4
// gcc (Linux) / clang (macOS) / MinGW-w64 (Windows)
// VS Code + C/C++ extension (tasks.json compiles)
// CLion / Visual Studio / Xcode
// Verify: $ gcc --version

命令行参数

main 可以接收用户在命令行输入的内容:argc 是参数个数,argv[] 是参数本身——argv[0] 永远是程序名。把参数当字符串解析,需要数值时用 strtol/strtod 转换。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(int argc, char **argv) {
printf("%d args\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
// $ ./prog a b c -> argv[0]=./prog argv[1]=a ...

多文件

大型程序拆成多个 .c 文件,配一个共享的 .h 头文件放声明。每个 .c 文件单独编译后链接;头文件让声明与定义保持一致,签名一改,所有用到它的地方都会同时暴露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// utils.h — declarations (prototypes)
#ifndef UTILS_H
#define UTILS_H
int add(int a, int b);
#endif
//
// utils.c — definitions
#include "utils.h"
int add(int a, int b) { return a + b; }
//
// main.c
#include <stdio.h>
#include "utils.h"
int main(void) { printf("%d\n", add(2, 3)); return 0; }
//
// $ gcc main.c utils.c -o app

退出码

main 返回的值会传到 shell:0(EXIT_SUCCESS)表示成功,其他值表示失败。脚本和 CI 都依赖这个约定,所以退出码要有意义。Linux/macOS 用 $? 查看,Windows 用 %ERRORLEVEL%。

1
2
3
4
5
6
#include <stdlib.h>
int main(void) {
if (error) return EXIT_FAILURE; // 1
return EXIT_SUCCESS; // 0
}
// Shell: $ echo $? (Linux/macOS) or %ERRORLEVEL% (Windows)

环境变量

getenv() 读取进程环境里的变量,返回其值,未设置时返回 NULL。它是不重新编译就注入配置的标准做法——但要把读到的值当作不可信输入。

1
2
3
4
#include <stdlib.h>
#include <stdio.h>
const char *path = getenv("PATH");
if (path) printf("%s\n", path); // may be NULL if unset

预处理器基础

预处理器在编译之前运行:#define 定义宏,# 和 ## 连接记号,#ifdef/#if 条件编译代码。它是纯文本、无类型的一层——多数场合优先用真正的函数和 const。

1
2
3
4
5
6
7
8
9
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define STR(x) #x // "x" as string
#define CONCAT(a, b) a##b // tokens joined
#ifdef DEBUG
printf("debug build\n");
#endif
#if defined(__cplusplus)
// compiled as C++
#endif

2.变量与常量

声明、类型大小、定宽整数、const 位置、存储类、typedef 与枚举。

基本声明

声明一个变量要先写类型再写名字,并在声明处初始化(C99+)。声明时初始化,避免后面读到未初始化的垃圾值。

1
2
3
4
int counter = 0;
double pi = 3.14159;
char letter = 'A';
_Bool flag = 1; // or <stdbool.h> -> bool

类型大小(64 位典型值)

C 只保证最小大小,int 在不同平台上可能是 16 位或 32 位。用 sizeof 打印目标平台的实际大小,保持诚实,绝不硬编码字节数。

1
2
3
4
5
6
7
8
9
// char 1 byte (-128..127)
// short 2 bytes (-32,768..32,767)
// int 4 bytes
// long 8 bytes (4 on Windows)
// long long 8 bytes (always >= 64 bit)
// float 4 bytes (6-7 sig digits)
// double 8 bytes (15-16 sig digits)
printf("int=%zu long=%zu ptr=%zu\n",
sizeof(int), sizeof(long), sizeof(void*));

定宽类型(C99+,<stdint.h>)

当内存布局必须精确——文件格式、网络协议、哈希——就用 <stdint.h> 里的 int32_t/uint64_t 等。它们只在平台确实提供时才存在,这正是精确宽度名字受青睐的原因。

1
2
3
4
5
6
#include <stdint.h>
int32_t exact = 42; // exactly 32 bits
uint64_t big = 1ULL << 40;
int_least16_t small; // >= 16 bits
intptr_t addr; // big enough for a pointer
printf("%d\n", (int)sizeof(int32_t)); // always 4

无符号 / 有符号

无符号类型永不为负,按 2^N 取模回绕;有符号类型可为负。两者混在一个表达式里时,有符号操作数会被提升为无符号——这是经典坑源(见 faq 节)。

1
2
3
unsigned int u = 4000000000U;
signed long long s = -9LL;
// Mixing signed + unsigned promotes to unsigned — beware (see faq)

const——只读(编译期检查)

const 让变量只读,编译器在构建期就能拒绝意外的写入。凡是不得改变的东西都加上 const:它既说明意图,也利于编译器优化。

1
2
const int MAX = 100;
// MAX = 5; // ERROR

const 指针位置(经典易混点)

指针声明里 const 的位置决定保护谁:在 * 之前保护指向的值,在 * 之后保护指针本身。从右往左读类型,就能看懂这两行的差别。

1
2
3
const int *p1; // pointer to const int — *p1 read-only, p1 reassignable
int *const p2; // const pointer to int — p2 read-only, *p2 writable
const int *const p3; // both read-only

存储类别

存储类别决定变量存在哪里、活多久。auto 是局部变量的默认;static 加在文件作用域上让名字不跨文件可见;static 加在局部变量上让它在多次调用间保值;extern 引用别处定义的名字。

1
2
3
4
5
static int file_scope = 0; // internal linkage (file-local)
extern int from_other; // defined in another file
static void helper(void) {} // file-local function
// 'static' on a local: persists across calls
void count(void) { static int n = 0; n++; printf("%d\n", n); }

typedef——类型别名

typedef 给类型起新名字。它不是创建新类型,只是别名——但有了别名就能写更短、更一致的类型,把 struct 和函数指针的噪音藏进一个可读的词。

1
2
3
typedef unsigned long size_type;
typedef struct { int x, y; } Point; // anonymous struct + alias
Point p = { 1, 2 };

enum——命名常量

enum 给一组相关的整数常量起名字。值从 0 开始逐个加 1,但也可以显式赋值或指定范围。枚举自带文档属性,还能用于 switch。

1
2
3
enum Color { RED, GREEN = 5, BLUE }; // 0, 5, 6
typedef enum { OK, WARN, ERR } Status;
Status s = OK;

指定初始化(C99+)

C99 允许按名字初始化成员(.port = 8080)或按下标初始化数组元素([3] = 9),顺序任意。没提到的成员自动清零——比一长串位置初始化清晰得多。

1
2
3
4
5
int arr[5] = { [0] = 1, [3] = 9 }; // 1 0 0 9 0
struct Config cfg = {
.host = "localhost",
.port = 8080,
};

复合字面量(C99+)

复合字面量用类似强转的语法内联构建一个临时 struct/数组值。想给函数传一次性值时,不用先声明具名变量,很方便。

1
2
void draw(struct Point pt);
draw((struct Point){ .x = 3, .y = 4 });

volatile(内存映射 I/O / 信号)

volatile 告诉编译器某个变量可能在程序正常流程之外改变——硬件寄存器、信号处理器或另一个线程。它阻止编译器对访问做缓存或重排。

1
volatile uint32_t *reg = (uint32_t *)0xFFFF0000;

3.结构体、联合体与聚合类型

结构体布局、联合体、位域、sizeof、_Static_assert 与对齐 —— 真实 C 数据的构建块。

struct——异构记录

struct 把不同类型但互相关联的值归成一个记录。成员按声明顺序存放,中间会插入对齐填充;所以 sizeof(struct) 常常大于成员大小之和。

1
2
3
4
struct Point { int x; int y; };
struct Point p = { 1, 2 }; // positional
struct Point q = { .y = 5 }; // designated (rest zero)
p.x = 10;

指向 struct 的指针——用 -> 访问成员

箭头 p->member 是 (*p).member 的简写,对结构体指针解引用访问成员。按指针传结构体很省(一个字),还能让函数修改调用者的记录。

1
2
struct Point *pp = &p;
pp->x = 20; // same as (*pp).x

匿名 struct + typedef

可以给没有标签的 struct 加 typedef,一条声明就得到干净的类型名。这是给常用记录类型定义名字的习惯做法,省得处处写 struct。

1
2
3
struct { int a, b; } ab; // unnamed type, single var
typedef struct { int a, b; } Pair;
Pair pr = { 3, 4 };

union——重叠存储(大小 = 最大成员)

union 把多个成员放进同一块内存,大小取最大成员。只有最后写入的那个成员才有效——读其他成员是未定义行为,所以要配一个标签字段。

1
2
3
4
5
6
7
union Value {
int i;
float f;
char bytes[4];
};
union Value v = { .f = 3.14f };
// v.i and v.f share the same memory (implementation-defined reading)

位域——打包标志位

位域把若干小标志打包进一个整数的比特位,为含大量布尔值的记录省内存。布局由编译器决定,所以位域只留在单个编译单元内,不要跨 ABI 边界。

1
2
3
4
5
6
struct Flags {
unsigned int ready : 1;
unsigned int error : 1;
unsigned int mode : 3;
};
struct Flags f = { 1, 0, 2 };

sizeof——对象占用的字节数

sizeof 给出类型或对象占用的字节数。对仍在作用域内的数组,sizeof a / sizeof a[0] 可得到元素个数——但一旦数组退化为指针,这个技巧就悄悄失效(见 faq)。

1
2
3
4
5
size_t n = sizeof(int); // 4 on typical systems
size_t sz = sizeof p; // sizeof(struct Point)
int arr[10];
size_t len = sizeof arr / sizeof arr[0]; // 10 (array length)
// NOTE: sizeof(arr) inside a function that received it decays (see pointers)

_Static_assert(C11+)编译期检查

_Static_assert(cond, msg) 在编译期断言,cond 为假就构建失败。它适合在尺寸和 ABI 假设被破坏的那一刻就校验,而不是等到运行时才发现。

1
2
_Static_assert(sizeof(void*) == 8, "64-bit only");
_Static_assert(sizeof(int) >= 4, "int too small");

对齐——& _Alignof(C11+)

对齐决定对象可放在内存的哪个地址;编译器给 struct 填充空隙让每个成员自然对齐。_Alignof 报告对象的对齐值,_Alignas 在需要时提高它(SIMD、缓存行共享)。

1
2
3
4
5
#include <stdalign.h>
_Alignof(int) // alignment requirement, e.g. 4
struct S { char c; int i; }; // padded: sizeof(S) often 8, not 5
// Force packing (non-portable, avoid unless needed):
// #pragma pack(push, 1) ... #pragma pack(pop)

柔性数组成员(C99+)——struct 尾部数组

struct 可以以一个不定长数组结尾(data[]),长度在分配时决定:一次 malloc 分配 struct 加额外字节。这是运行时才知道缓冲区大小的 C 惯用法。

1
2
3
4
5
6
struct Buffer {
size_t len;
char data[]; // last member only
};
struct Buffer *buf = malloc(sizeof(*buf) + 100);
buf->len = 100;

枚举作为独立类型(C23)

C23 允许固定枚举的底层类型(enum Status : unsigned char),大小不再由编译器决定。否则枚举与 int 兼容,这对多数代码都够用。

1
2
3
enum Status { ST_OK = 0, ST_ERR = 1 };
// C23 lets you pin the type:
// enum Status : unsigned char { ST_OK = 0, ST_ERR = 1 };

4.指针与数组

取地址、解引用、指针算术、数组衰减、指针 vs 数组、const 正确性。

取地址(&)与解引用(*)

& 取对象的地址得到指针;* 反向操作,通过指针读写。两者配合让你能间接访问一个值,并在另一个作用域里修改它。

1
2
3
4
int x = 42;
int *p = &x; // p holds the address of x
printf("%d\n", *p); // 42 (*p reads x)
*p = 7; // writes through the pointer -> x == 7

NULL——空指针

NULL 是“什么都不指向”的指针,不同于任何合法地址。解引用它是未定义行为,通常直接崩溃——所以用之前务必判断是否为 NULL。

1
2
3
4
int *p = NULL;
if (p) { /* non-null */ }
if (!p) { /* null */ }
// Dereferencing NULL is undefined behavior — always check.

指针算术(按 sizeof(*p) 缩放)

指针加整数是按元素前进,不是按字节——p + 2 跳过两个 int。下标 p[i] 其实就是 *(p + i)。越过数组末尾是未定义行为。

1
2
3
4
5
int arr[5] = { 10, 20, 30, 40, 50 };
int *p = arr; // == &arr[0]
*(p + 2) // arr[2] == 30
p[2] // same — [] is sugar for *(p + n)
*(p + 5) // UB — past the end

数组退化(表达式变指针)

多数表达式中,数组名变成指向首元素的指针。后果是:在函数内部无法从参数恢复数组长度——必须把长度和数组一起传进来。

1
2
3
4
5
void sum(int *a, int n);
int data[10];
sum(data, 10); // 'data' decays to int* — length must be passed
// sizeof(data) == 40 here (true array);
// inside sum(), sizeof(a) == 8 (pointer) — see faq

指针 vs 数组

char 数组自己拥有字节,可修改;指向字符串字面量的 char* 通常指向只读内存。透过字面量指针写入是未定义行为,常常崩溃。

1
2
3
4
// char s[] = "hi"; mutable array, 3 bytes (incl '\0')
// char *p = "hi"; pointer to string literal (read-only in practice)
s[0] = 'H'; // OK for array
// p[0] = 'H'; // UB (literal usually in read-only memory)

多级指针

指向指针的指针(int**)多了一层间接。它用于字符串数组、二维结构,以及“由函数分配内存、调用者负责释放”的输出参数。

1
2
3
4
int x = 1;
int *p = &x;
int **pp = &p; // pointer to pointer
**pp = 2; // writes x

函数指针

函数指针存的是函数的地址,可以间接调用。它是回调、分派表,以及 oop 节里那些面向对象模式的基石。

1
2
3
4
5
6
7
int add(int a, int b) { return a + b; }
int (*fp)(int, int) = add; // pointer to function
fp(1, 2); // call through it
// Cleaner with typedef:
typedef int (*BinOp)(int, int);
BinOp op = add;
op(2, 3);

指针数组(如 argv)

char* 数组是存放字符串列表的惯用容器,argv 就是一个。每个元素指向各自以 NUL 结尾的字符串,数组本身只是一串指针。

1
2
3
const char *names[] = { "alice", "bob" };
// pointer-to-array (rare):
int (*row)[5]; // pointer to int[5]

const 正确性

把指针参数声明为 const——例如 const char* s——等于承诺函数不会改动指向的内容。调用者就能放心传字符串字面量和数组,编译器会强制这份承诺。

1
2
3
void f(const int *p); // promise not to modify *p
int arr[5] = {1,2,3,4,5};
f(arr); // int[] decays to int*, fits const int*

数组指针 vs 指针数组

int *a[5] 是 5 个指针组成的数组;int (*b)[5] 是指向 5 个 int 数组的指针。括号改变一切。复杂声明从右往左读,或者用 cdecl 解码。

1
2
3
4
5
// int *a[5]; array of 5 pointers (to int)
// int (*b)[5]; pointer to an array of 5 ints
// int (*fn)(int, int); function pointer
// int *fn(int, int); function returning int*
// Use a parser / cdecl tool: https://cdecl.org

5.流程控制

if/else、三元、switch、for、while、do-while、break/continue、goto 与 setjmp/longjmp。

if / else if / else

按条件分支:第一个条件为真的分支执行,否则走 else。else-if 链按顺序测试多个备选——命中第一个即停。

1
2
3
4
5
6
7
if (n > 0) {
puts("positive");
} else if (n == 0) {
puts("zero");
} else {
puts("negative");
}

三目运算符(表达式)

cond ? a : b 是表达式而非语句:按 cond 取 a 或 b。适合紧凑的取值选择;如果一行放不下、读不清,就改用 if/else。

1
2
const char *sign = n > 0 ? "pos" : "non-pos";
int max = (a > b) ? a : b;

switch——不 break 就穿透

switch 按整数值分派。除非 break,执行会穿透进下一个 case——要么有意利用穿透(空 case 堆叠),要么每个 case 都 break。default 处理未匹配的值。

1
2
3
4
5
6
7
8
9
10
11
switch (status) {
case 200:
case 201:
puts("ok");
break;
case 404:
puts("missing");
break;
default:
puts("other");
}

for——在 init 中声明计数器(C99+)

for(init; cond; step) 是标准计数循环。在 init 部分声明计数器(C99+)能把它限定在循环内,避免和外层同名变量冲突。

1
2
3
4
5
for (int i = 0; i < 10; i++) {
printf("%d ", i);
}
// Empty init / decrement: while-style
for (;;) { if (stop) break; }

while / do-while

while 每次迭代前测试条件,可能一次都不执行。do-while 总是先执行一次再测试——适合菜单和校验提示这类“至少问一次”的场景。

1
2
3
4
while (n < 10) { n++; }
do {
process(input);
} while (!done); // body runs at least once

break / continue

break 立即退出最内层循环或 switch;continue 跳到下一次迭代。用好了能摊平嵌套逻辑、去掉标志变量;用坏了会隐藏控制流,所以尽量少层。

1
2
3
4
for (int i = 0; i < 10; i++) {
if (i == 3) continue; // skip 3
if (i == 7) break; // stop at 7
}

goto——跳出深层嵌套(少用、慎用)

goto 跳到某行标签。它合理的用途是跳出深层嵌套、集中做错误清理——即 goto-cleanup 惯用法。除此之外,return 或标志变量几乎总是更清晰。

1
2
3
4
5
6
7
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (found(i, j)) goto done;
}
}
done:
printf("found at %d,%d\n", i, j);

setjmp / longjmp 非局部跳转

setjmp 保存执行上下文,longjmp 从任意位置恢复它,一路回卷到保存点。途中所有清理都被跳过——没有析构函数会执行——所以比起 goto 或显式 return,通常不划算。

1
2
3
4
5
6
7
8
#include <setjmp.h>
jmp_buf env;
if (setjmp(env) == 0) {
risky(); // may longjmp back here
} else {
puts("recovered");
}
// longjmp(env, 1) from anywhere restores the saved context

循环惯用法

值得认识的循环形态:对计数范围的下标循环、对数组的指针遍历、以及带 break 的无限循环。选最能直接表达意图的那种。

1
2
3
for (size_t i = 0; i < n; i++) // index
for (const int *p = arr; p < arr + n; p++) // pointer walk
for (;;) { if (eof) break; } // forever + break

逻辑短路求值

&& 和 || 从左到右求值,结果一确定就停止。这就是 ptr && ptr->x 安全的原因:第二个操作数只在第一个为真时才执行,所以可以在行内守卫解引用。

1
2
if (ptr && ptr->val > 0) // safe: short-circuits on NULL
if (a || b) // b not evaluated if a is true

6.函数与可变参数

传值 vs 传指针、函数指针与 typedef、可变参数、递归与 _Generic。

按值传递——调用者的变量不变

C 所有参数都按值传递——被调函数拿到的是副本,修改它不影响调用者。简单的标量和小的结构体这样传;需要修改或担心大对象时用指针形式。

1
int add(int a, int b) { return a + b; }

按指针传递(修改调用者)

要修改调用者的变量,就传它的地址。被调函数解引用指针并写入,改动就对调用者可见——scanf 这类函数正是这样产生输出的。

1
2
3
4
void swap(int *a, int *b) {
int t = *a; *a = *b; *b = t;
}
swap(&x, &y);

const 参数——只读契约

const 参数如 const char* s 是只读契约:函数承诺不修改其指向的内容。它让调用者能安全传字符串字面量,也让 API 意图一目了然。

1
2
size_t len(const char *s) { return strlen(s); }
// Also lets callers pass string literals.

数组参数退化(需传长度)

数组参数会退化为指针,长度不会跟着传过去。务必显式传长度——否则函数无从知道数据在哪里结束。

1
2
3
4
5
int sum(const int *xs, size_t n) {
int total = 0;
for (size_t i = 0; i < n; i++) total += xs[i];
return total;
}

函数指针 + typedef

给函数指针签名加一次 typedef,之后用别名声明变量和参数。这样分派表和回调读起来不再是一堵括号加星号的墙。

1
2
3
4
5
6
7
8
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
typedef int (*BinOp)(int, int);
int apply(BinOp op, int a, int b) { return op(a, b); }
int r = apply(add, 3, 4);
// Table of function pointers (mini dispatch):
BinOp ops[] = { add, mul };
int s = ops[1](3, 4); // 12

可变参数——printf 风格函数

签名带 ... 的函数接收可变数量的参数,如 printf。用 va_start/va_arg/va_end 读取;固定的参数必须说明后面还有多少,否则调用无法被安全解码。

1
2
3
4
5
6
7
8
9
10
11
#include <stdarg.h>
#include <stdio.h>
int sum(int count, ...) {
va_list ap;
va_start(ap, count);
int total = 0;
for (int i = 0; i < count; i++) total += va_arg(ap, int);
va_end(ap); // must pair with va_start
return total;
}
int n = sum(4, 10, 20, 30, 40); // 100

可变参数转发——v* 系列函数

要把可变参数转发给另一个可变参数函数(比如日志封装转发给 vfprintf),用 v* 系列并把 va_list 本身传过去。再用 va_arg 重读会破坏列表。

1
2
3
4
5
6
void logf(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}

递归

递归函数在更小的同类问题上调用自身,直到触及基线。每次调用都占栈空间,所以深度大或不可控时优先用迭代。

1
2
3
4
5
6
7
int fact(int n) { return n < 2 ? 1 : n * fact(n - 1); }
// Iterative is usually safer (no stack overflow):
long fib_iter(int n) {
long a = 0, b = 1;
for (int i = 0; i < n; i++) { long t = a; a = b; b = t + b; }
return a;
}

_Generic(C11+)——编译期类型分派

_Generic(C11)根据控制表达式的类型,在编译期选出结果表达式——一种轻量模拟函数重载、写类型安全宏的办法。

1
2
3
4
5
6
#define abs_val(x) _Generic((x), \
int: abs_i, \
long: abs_l, \
double: abs_d, \
default: abs_x)(x)
int abs_i(int x); long abs_l(long x); double abs_d(double x);

noreturn(C11+)与 inline

noreturn 标记永不返回的函数(如致命错误例程),编译器因此能推断不可达代码。inline 只是请求而非保证——编译器仍可能发出真实的调用。

1
2
3
4
5
6
#include <stdnoreturn.h>
noreturn void die(const char *msg) {
fprintf(stderr, "%s\n", msg);
exit(EXIT_FAILURE);
}
inline int square(int x) { return x * x; } // hint to inline

7.字符串与数值转换

NUL 结尾字符串、安全写入、strtol/strtok/sscanf 转换与 printf 格式符表。

基础——以 NUL 结尾的字符数组

C 字符串是以 NUL 字节('\0')结尾的字符数组。strlen 数到终止符为止;任何装字符串的缓冲区都得给它留位置,否则写入就溢出。

1
2
3
4
#include <string.h>
#include <stdio.h>
char s[] = "hello"; // 6 bytes incl. '\0'
size_t n = strlen(s); // 5 (excludes '\0')

复制 / 比较 / 拼接(不安全 vs 安全)

strcpy/strcat 不检查边界,源串太大时会静默溢出。优先用 snprintf 做有界、总是以 NUL 结尾的写入;strcmp/strncmp 比较;strncat 最多复制 n 个字符再加终止符。

1
2
3
4
5
6
7
strcpy(dst, src); // UNSAFE if src too big
strncpy(buf, src, sizeof buf - 1);
buf[sizeof buf - 1] = '\0'; // strncpy may not NUL-terminate
strcmp(a, b) // <0, 0, >0
strncmp(a, b, 4) // compare first 4 chars
strcat(buf, src); // appends (UNSAFE)
snprintf(buf, sizeof buf, "%s%s", a, b); // SAFE build

查找

strchr 找字符首次出现,strrchr 找最后一次,strstr 找子串首次出现。都返回指向字符串内部的指针,找不到返回 NULL。

1
2
3
char *p = strchr(buf, 'w'); // first 'w'
char *q = strstr(buf, "lo wo"); // substring
char *r = strrchr(buf, 'o'); // last 'o'

mem*——原始字节(可处理任意内存,含 NUL)

mem* 函数(memcpy、memmove、memset、memcmp)处理原始字节,能应对内嵌的 NUL。memcpy 和 memmove 的差别在于是否容忍重叠——可能重叠时用 memmove。

1
2
3
4
memcpy(dst, src, n); // no overlap allowed
memmove(dst, src, n); // safe if overlapping
memset(arr, 0, sizeof arr);
int eq = memcmp(a, b, n);

数值转换:strtol/strtod

用 strtol/strtod 解析数值:它们会报错并指出解析停在哪。atoi 遇到垃圾静默返回 0,会掩盖非法输入——追求正确的代码别用它。

1
2
3
4
5
6
7
#include <stdlib.h>
char *end;
errno = 0;
long v = strtol("42abc", &end, 10); // base 10 -> 42, end -> "abc"
long h = strtoll("0x1F", NULL, 16); // hex -> 31
double d = strtod("3.14e2", NULL); // -> 314.0
// atoi has no error detection: atoi("abc") == 0 silently

健壮解析——校验整个字符串

一次完整的解析要校验三件事:strtol 后 errno 未置位、至少消耗了一位数字(end != s)、整个字符串都被读完(*end == '\0')。跳过任何一条,尾部垃圾就溜进来了。

1
2
3
4
5
6
7
8
9
int parse_int(const char *s, long *out) {
if (!s || !out) return -1;
errno = 0;
char *end = NULL;
long v = strtol(s, &end, 10);
if (errno || end == s || *end != '\0') return -1;
*out = v;
return 0;
}

分词:strtok_r 可重入

strtok 会原地修改字符串,在每个分隔符处插入 '\0';带 _r 后缀的版本可重入(线程安全)。还需要原字符串的话,先复制一份。

1
2
3
4
5
char line[] = "a,b,c";
char *save = NULL;
for (char *tok = strtok_r(line, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) {
puts(tok);
}

sscanf——用格式串解析(返回匹配数)

sscanf 用格式串解析字符串,返回成功匹配的字段数。检查这个返回值:比预期低,说明输入不是你以为的形状。

1
2
3
int year, month;
if (sscanf("2026-08", "%d-%d", &year, &month) == 2) { /* ok */ }
// %d %f %s %c %zu %x %n (%% = literal %)

printf 格式表

printf 家族共用一套格式语言:%d int、%ld long、%zu size_t、%f double、%s 字符串、%p 指针,加上宽度和精度修饰符。格式符必须与实参类型精确匹配——不匹配是未定义行为。

1
2
3
4
5
6
7
8
// %d int %ld long %lld long long
// %u unsigned int %zu size_t %zd ssize_t
// %f double %lf double %e scientific
// %.2f 2 decimals %x / %X hex %o octal
// %c char %s string %p pointer
// %5d pad to 5 width %-5d left-justify %05d zero-pad
printf("%s=%d\n", "count", 42);
printf("%.2f %%\n", 3.14159); // 3.14 % (%% = literal %)

安全地拼接字符串

snprintf 最多写 n 字节且总是以 NUL 结尾,是安全拼字符串的办法。对不断增长的文字,记住长度并 realloc 缓冲区,或用一个小型动态缓冲辅助函数。

1
2
3
4
char buf[64];
snprintf(buf, sizeof buf, "user-%d", id);
// Grow-able: use strcat carefully or implement a dynamic buffer.
// For heavy string work consider OpenSSL BIO or a small dyn-str helper.

8.数组、排序与查找

数组遍历、带比较器的 qsort/bsearch、二维数组与结构体数组。

定长数组——带长度迭代

C 数组是一块固定大小的连续元素。用长度迭代:真正的数组仍在作用域内时 sizeof a / sizeof *a 有效,但一旦传给函数,大小就丢了。

1
2
3
4
int arr[5] = { 3, 1, 4, 1, 5 };
for (size_t i = 0; i < sizeof arr / sizeof *arr; i++) {
printf("%d ", arr[i]);
}

初始化模式

数组可按位置、按指定下标([2] = 9)、或整体清零({0})初始化。二维数组是数组的数组,行主序存放:m[i][j] 在每行内连续。

1
2
3
int zeros[10] = { 0 }; // all zero
int spec[5] = { [0] = 9, [4] = 1 }; // designated
int grid[2][3] = { {1,2,3}, {4,5,6} }; // 2-D

qsort——用比较器排序

qsort 用比较器原地排序,比较器返回 <0、0 或 >0。比较器收到的是指向元素的指针,所以先把 void* 参数转回元素类型再安全比较(避免溢出)。

1
2
3
4
5
6
7
8
9
#include <stdlib.h>
#include <stdio.h>
int cmp_int(const void *a, const void *b) {
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y); // safe for big/small ints
}
int nums[] = { 5, 2, 9, 1 };
qsort(nums, 4, sizeof(int), cmp_int); // 1 2 5 9

bsearch——在已排序数组上二分查找

bsearch 做二分查找——但只对已经排序的数组。它返回指向匹配元素的指针,找不到返回 NULL。大数组上先排序再查找,胜过线性扫描。

1
2
3
int key = 5;
int *hit = bsearch(&key, nums, 4, sizeof(int), cmp_int);
if (hit) printf("found %d\n", *hit);

字符串比较器(char* 数组)

给 char* 数组排序必须比较字符串而不是指针值。比较器转成 char* const*,解引用拿到每个字符串指针,再调用 strcmp。

1
2
3
4
5
6
7
int cmp_str(const void *a, const void *b) {
const char *x = *(const char *const *)a;
const char *y = *(const char *const *)b;
return strcmp(x, y);
}
const char *names[] = { "bob", "alice" };
qsort(names, 2, sizeof(char*), cmp_str);

结构体数组 + 按字段排序

要按某个字段给结构体数组排序,写一个接收两个结构体指针、比较该字段的比较器(比如 a->key 对 b->key 三路比较)。qsort 就会重排整个记录。

1
2
3
4
5
6
7
struct Item { int key; const char *name; };
int cmp_item(const void *a, const void *b) {
const struct Item *x = a, *y = b;
return (x->key > y->key) - (x->key < y->key);
}
struct Item items[] = { {2,"b"}, {1,"a"}, {3,"c"} };
qsort(items, 3, sizeof(struct Item), cmp_item);

数组指针参数(保持二维形状)

把二维数组传给函数,参数类型里必须带列数——int m[][COLS]——编译器才能算出每行起点。第一维(行数)可以自由变化。

1
void print_grid(int rows, int cols, int m[][cols]);

动态数组模式(另见 mem 节)

可增长集合是 malloc 出来的块,满了就 realloc:容量和长度分开记,增长时翻倍,用完 free。这是高层语言里 vector/list 在 C 中的对应物。

1
2
3
4
5
6
#include <stdlib.h>
int *dyn = malloc(10 * sizeof(int));
for (int i = 0; i < 10; i++) dyn[i] = i;
// grow:
dyn = realloc(dyn, 20 * sizeof(int));
free(dyn);

变长数组(C99 可选)

变长数组按运行时值在栈上分配(int vla[n])。C11 中它是可选的,n 太大时可能撑爆栈——大的或需要可移植的东西优先用 malloc。

1
// int n = 8; int vla[n]; // stack array sized at runtime

二维数组——行主序

int m[ROWS][COLS] 是连续的行主序块;m[i][j] 就是 *(*(m + i) + j)。下标需要知道行长才能算出行起点,所以形状必须在编译期确定。

1
2
3
4
5
6
7
#define ROWS 2
#define COLS 3
int m[ROWS][COLS] = { {1,2,3}, {4,5,6} };
for (int i = 0; i < ROWS; i++)
for (int j = 0; j < COLS; j++)
printf("%d ", m[i][j]);
// Contiguous: m[i][j] == *(*(m + i) + j)

9.动态内存与所有权

malloc/calloc/realloc/free、栈 vs 堆、对齐、所有权规则与 goto cleanup 模式。

分配与释放

malloc 返回未初始化的堆内存,每块分配最终都要释放。用前务必检查是否为 NULL;释放后把指针置 NULL,之后的重复释放就成了安全的空操作。

1
2
3
4
5
6
#include <stdlib.h>
#include <string.h>
int *p = malloc(10 * sizeof(int)); // uninitialized
if (!p) { perror("malloc"); exit(EXIT_FAILURE); } // ALWAYS check
free(p); // after free, p is dangling
p = NULL; // avoid use-after-free

calloc:count×size 清零

calloc(count, size) 会清零内存,并替你相乘两个参数,还能检测溢出。需要已初始化存储且元素个数已知时优先用它。

1
int *z = calloc(10, sizeof(int)); // all zeros

realloc——调整大小;可能移动内存块

realloc 调整分配的大小,可能把块搬到新地址。先用临时变量接收返回值并检查:失败时旧块仍然有效,但如果你先覆盖了指针,它就泄漏了。

1
2
3
4
5
int *q = malloc(4 * sizeof(int));
int *tmp = realloc(q, 8 * sizeof(int));
if (tmp) { q = tmp; } // on failure old block still valid
else { /* keep q, handle error */ }
free(q);

栈 vs 堆

栈内存自动管理、快、返回时回收,但小且短命。堆内存手动管理、大、能活过函数。又大又长寿的缓冲区该放在堆上。

1
2
3
// Stack: fast, auto-freed, small (MBs), fixed size at compile/runtime
// Heap: large, manual free, survives return, slower
// int local[1'000'000]; may overflow the stack -> use malloc

所有权——一组简单规则

一组最少的所有权纪律能让内存问题可控:谁分配谁释放;每个指针只释放一次;释放后置 NULL;realloc 失败时保留旧指针。约定就是 C 唯一的资源管理器。

1
2
3
4
// 1) Whoever allocates frees (or documents otherwise)
// 2) Free once, never free a non-heap pointer
// 3) After free set pointer to NULL to catch double-free
// 4) Realloc failure: keep the old pointer

堆上的字符串

堆上字符串需要 strlen(s) + 1 字节——多出来的是 NUL 终止符。malloc(strlen(s) + 1) 再复制;忘了 +1 就是一个待引爆的一字节溢出。

1
2
3
char *name = malloc(strlen("hello") + 1);
strcpy(name, "hello");
free(name);

含内部指针的 struct

拥有堆数据的 struct 有两块分配要释放:先内层缓冲区,再 struct 本身。决定并写明谁拥有内部指针,按分配的逆序释放。

1
2
3
4
5
6
7
struct Line { char *data; size_t len; };
struct Line *line = malloc(sizeof *line);
line->data = malloc(100);
line->len = 0;
// free order: innermost first
free(line->data);
free(line);

goto cleanup(单一出口)

C 没有析构函数,所以 goto-cleanup 就是 C 的 RAII:每个函数一个出口、一个清理块,所有路径都到达它。每个资源在每条路径上恰好释放一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int process(const char *path) {
FILE *f = NULL;
char *buf = NULL;
int rc = -1;
f = fopen(path, "r");
if (!f) { perror("fopen"); goto cleanup; }
buf = malloc(4096);
if (!buf) { perror("malloc"); goto cleanup; }
if (fread(buf, 1, 4096, f) == 0) goto cleanup;
// ... work ...
rc = 0;
cleanup:
free(buf);
if (f) fclose(f);
return rc; // single return
}

对齐 / 超对齐(C11)

aligned_alloc 能按指定对齐申请内存(SIMD、缓存行共享),大小必须是对齐值的整数倍。普通 malloc 只保证平台的默认对齐。

1
2
3
#include <stdlib.h>
// aligned_alloc(size, alignment) — size must be a multiple of alignment
// double *a = aligned_alloc(alignof(double) * 4, alignof(double) * 4);

内存泄漏 / 溢出检测工具(见 build 节)

泄漏、溢出和释放后使用都藏到进程死掉才暴露——用 valgrind 或 AddressSanitizer 机械地找出来。两者都在 build 节讲过,是你能加的最便宜的安全网。

1
2
3
// valgrind ./prog
// gcc -fsanitize=address,undefined -g prog.c -o prog
// AddressSanitizer catches leaks, OOB, use-after-free at runtime

10.面向对象风格模式

C 是过程式语言,但结构体 + 函数指针可模拟类、继承与多态。

“类” = struct + vtable

C 模拟类:用 struct 装数据,用一张函数指针表(vtable)装行为。方法就是带显式 self/this 指针参数的普通函数。

1
2
3
4
5
6
7
8
9
struct Animal {
const char *name;
void (*speak)(const struct Animal *self);
};
static void dog_speak(const struct Animal *a) {
printf("%s: woof\n", a->name);
}
struct Animal rex = { "Rex", dog_speak };
rex.speak(&rex);

通过内嵌实现继承

把基类 struct 内嵌为第一个成员就得到继承:派生 struct 的地址等于基类的地址,所以派生值可以在任何期望基类的地方使用,基类方法原样生效。

1
2
3
struct Dog { struct Animal base; int breed; };
struct Dog d = { { "Rex", dog_speak }, 1 };
d.base.speak(&d.base);

构造函数 / 析构函数配对

配一个负责分配并初始化的 init 函数和一个负责释放并回收的 free 函数——C 版的构造/析构函数。每个被创建的对象都必须恰好销毁一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Vec {
int *data;
size_t len, cap;
};
struct Vec *vec_new(void) {
struct Vec *v = malloc(sizeof *v);
if (v) { v->data = NULL; v->len = v->cap = 0; }
return v;
}
void vec_free(struct Vec *v) {
free(v->data);
free(v);
}

不透明指针——隐藏实现(PIMPL)

头文件里只前置声明 struct,定义只放在 .c 文件里。调用者能持有指针但看不到内部——这就是 PIMPL,隐藏实现,让内部改动不破坏调用者。

1
2
3
4
5
6
7
8
9
// foo.h:
struct Foo; // forward declaration
typedef struct Foo Foo;
Foo *foo_new(void);
void foo_destroy(Foo *f);
int foo_get(const Foo *f);
// foo.c:
struct Foo { int value; }; // definition hidden
Foo *foo_new(void) { return calloc(1, sizeof(struct Foo)); }

接口 = 函数指针组成的 struct

装满函数指针的 struct 就是接口:任何填充它的实现都能插进来,每个函数再收一个 void* self 作为实例上下文。这就是 C 形态的多态。

1
2
3
4
5
struct Stream {
int (*read)(void *self, char *buf, int n);
int (*write)(void *self, const char *buf, int n);
void *self; // context
};

类型擦除数据(void *)

void* 抹掉类型,让一个容器能装异构的值。想安全地还原类型,通常得加一个标签字段记录指针真正指向什么。

1
2
typedef struct { void *data; } Box;
// + a tag for runtime type checks if needed

通过返回 *this 实现方法链

每个 setter 返回对象本身(*this),调用就能连起来:builder->setA(x)->setB(y)。它让 builder 风格配置变得紧凑,代价是返回值可以被忽略。

1
2
3
4
struct Builder *set_name(struct Builder *b, const char *n) {
b->name = n; return b;
}
set_name(set_name(new_builder(), "app"), "x");

单例(懒加载,线程不安全)

懒加载单例在首次使用时,把实例分配进一个 static 指针。这个简单版本不是线程安全的——两个线程可能竞争首次分配——一旦被共享就得加锁。

1
2
3
4
5
static Config *cfg = NULL;
Config *get_config(void) {
if (!cfg) cfg = config_new();
return cfg;
}

引用计数(简单版)

retain/release 给对象配一个引用计数,降到零就释放。它是 Objective-C 的基础,对 C 对象而言是比垃圾回收更轻的所有权模型。

1
2
void retain(Object *o) { o->refs++; }
void release(Object *o) { if (--o->refs == 0) free(o); }

完整 OOP 可行(glib)

C 里存在完整的 OOP 框架——glib 和 GObject 就是证明——但它们带来真实复杂度。最简的 vtable 加不透明指针就能覆盖多数需求;重武器留给真正需要的项目。

1
2
// Prefer a minimal vtable + opaque pointer.
// For callbacks: use void* context args to avoid globals.

11.错误处理

无异常:errno、返回码 + out 参数、goto cleanup、错误枚举与断言。

调用失败后的 errno

库函数和系统函数失败时设置全局 errno,你必须在调用后立刻读取或复制——任何后续调用都可能覆盖它。strerror(errno) 把错误码转成可读信息。

1
2
3
4
5
6
7
8
9
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
if (mkdir("foo", 0755) != 0) {
fprintf(stderr, "mkdir: %s\n", strerror(errno));
return 1;
}
// Always read errno immediately after the call (or copy it).

返回码 + 出参(常见 C 惯用法)

C 主流错误惯用法:成功返回 0,失败返回负错误码,结果通过出参指针写出。失败时结果指针保持 NULL/不动,调用者可以据此判断。

1
2
3
4
5
6
7
8
9
10
11
12
int parse_int(const char *s, long *out) {
if (!s || !out) return -1;
errno = 0;
char *end = NULL;
long v = strtol(s, &end, 10);
if (errno || end == s || *end != '\0') return -1;
*out = v;
return 0; // 0 == success
}
long n;
if (parse_int("42", &n) == 0) printf("got %ld\n", n);
else fprintf(stderr, "bad input\n");

错误枚举——显式错误分类

枚举你的函数可能产生的错误种类——OK、INVALID_INPUT、NOMEM、IO——并返回它们。具名错误码自带文档、适合 switch,还逼你想清楚每种失败模式。

1
2
3
4
5
enum Error { ERR_OK = 0, ERR_INVALID, ERR_NOMEM, ERR_IO };
enum Error do_thing(int x) {
if (x < 0) return ERR_INVALID;
return ERR_OK;
}

assert——调试用(NDEBUG 移除)

assert(cond) 在 cond 为假时中止程序,开发期抓 bug。它在 -DNDEBUG 下被编译掉,所以绝不能守卫用户依赖的行为——只校验你假设为真的不变式。

1
2
3
#include <assert.h>
assert(ptr != NULL);
assert(index < size && "index out of range");

perror——打印 errno 描述

perror 把你的消息连同当前 errno 的描述打印到 stderr 一行。这是报告系统调用失败原因的最快方式——errno 部分不用自己写格式串。

1
2
FILE *f = fopen(path, "r");
if (!f) { perror(path); return 1; } // 'path: No such file'

_Static_assert——编译期

_Static_assert 在编译期而不是运行期检查假设。用它校验尺寸、偏移和 ABI 约束,避免它们在生产二进制里静默失败。

1
_Static_assert(sizeof(int) == 4, "expected 4-byte int");

线程安全的 strerror_r(POSIX)

strerror 不是线程安全的,会覆写自己的内部缓冲区。strerror_r(POSIX)改写入调用者提供的缓冲区,并发线程各拿各的消息。

1
2
3
char ebuf[256];
strerror_r(errno, ebuf, sizeof ebuf);
fprintf(stderr, "err: %s\n", ebuf);

检查系统调用返回值

系统调用会失败——文件、套接字、内存都会。忽略返回值会把一次细微失败变成稍后的崩溃或数据损坏;fread/fwrite 即使成功也可能返回短计数,所以务必检查。

1
2
3
4
5
int fd = open(path, O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
// Check fread/fwrite: count may be short on errors
size_t got = fread(buf, 1, sizeof buf, f);
if (got < sizeof buf && ferror(f)) { /* real error */ }

用 ferror/feof 区分错误与文件末尾

读循环结束时,ferror() 和 feof() 告诉你原因:是真正的 I/O 错误还是到了文件末尾。EOF 是正常状态而非错误——用 ferror 区分两者。

1
2
if (ferror(f)) puts("read error");
else if (feof(f)) puts("end of file");

12.文件 I/O

stdio 流、逐行与整文件读取、二进制 I/O、POSIX 描述符、mmap 与目录遍历。

打开 / 关闭

fopen 返回 FILE* 流,失败返回 NULL;fclose 冲刷并释放它。模式是 r/w/a 加 + 表示读写、b 表示二进制。打开失败后绝不要继续用那个指针。

1
2
3
4
5
#include <stdio.h>
FILE *f = fopen("data.txt", "r");
if (!f) { perror("fopen"); return 1; }
// modes: "r" "w" "a" "r+" "w+" "a+" (+"b" for binary: "rb" "wb")
fclose(f);

读取整个文件(小文件)

小文件的做法:seek 到结尾、ftell 拿到大小、rewind、malloc size+1、再 fread 读全。确认读到的字节数与请求一致——调用之间文件可能变了。

1
2
3
4
5
6
7
fseek(f, 0, SEEK_END);
long sz = ftell(f);
rewind(f);
char *buf = malloc(sz + 1);
if (buf && fread(buf, 1, sz, f) == (size_t)sz) {
buf[sz] = '\0';
}

逐行读取(惯用法)

fgets 安全地读一行到固定缓冲区,停在换行符或缓冲区上限。用 strcspn 去掉结尾换行——它会被保留在缓冲区里。这是逐行处理文本的惯用方式。

1
2
3
4
5
char line[256];
while (fgets(line, sizeof line, f)) {
line[strcspn(line, "\r\n")] = '\0'; // strip trailing newline
process(line);
}

写入

fprintf/fputs/fputc 分别向流写格式化、字符串和字符输出。错误既出现在写入时也可能在 fclose 时,所以要检查 fclose 的结果,防止失败的冲刷被漏掉。

1
2
3
fprintf(f, "%s=%d\n", "key", 42);
fputs("line\n", f);
fputc('x', f);

先格式化到字符串(安全)

先用 snprintf 把整段输出拼进缓冲区,再一次写出去。这样能避免并发来源的交错写入、写前可校验,实际也保持写入原子性。

1
2
3
char out[64];
snprintf(out, sizeof out, "user-%d", id);
fputs(out, f);

二进制 I/O——读写原始字节

fread/fwrite 搬运原始字节,最适合定长记录:恰好读 sizeof(record) 并处理返回值。短读不是错误就是 EOF——用 ferror/feof 区分。

1
2
3
4
uint32_t header[4];
size_t n = fread(header, sizeof header[0], 4, f);
if (n != 4) { /* short read: ferror(f) or feof(f) */ }
fwrite(header, sizeof header[0], 4, out);

POSIX 文件描述符

stdio 之下,Linux/macOS 提供对 int 文件描述符的 open/read/write/close:控制精细、无缓冲、坑也更多。要跨界就用 fdopen(fd 换 FILE*)或 fileno(FILE* 换 fd)。

1
2
3
4
5
6
7
#include <unistd.h>
#include <fcntl.h>
int fd = open("data.bin", O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
char b[4096];
ssize_t got = read(fd, b, sizeof b); // may be < sizeof b
close(fd);

mmap——把文件映射进内存

mmap 把文件直接映射进地址空间,读写变成普通内存访问——大文件很快。munmap 释放它;文件不能为空,映射大小按页对齐。

1
2
3
4
5
6
7
8
9
#include <sys/mman.h>
#include <sys/stat.h>
int fd = open("big.bin", O_RDONLY);
struct stat st; fstat(fd, &st);
const void *data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (data == MAP_FAILED) { perror("mmap"); close(fd); return 1; }
// use data[0..st.st_size-1] ...
munmap((void *)data, st.st_size);
close(fd);

目录遍历

opendir/readdir/closedir 逐个列出目录项。跳过隐含的 '.' 和 '..' 条目,再用 stat 查每个名字的文件类型和大小。

1
2
3
4
5
6
7
#include <dirent.h>
DIR *d = opendir(".");
struct dirent *e;
while ((e = readdir(d))) {
if (e->d_name[0] != '.') printf("%s\n", e->d_name);
}
closedir(d);

stat——文件元数据

stat/lstat/fstat 返回文件的元数据——大小、修改时间、权限、类型。tmpfile() 给你一个匿名临时文件,关闭时自动删除,适合存临时数据。

1
2
3
4
5
6
7
8
9
10
#include <sys/stat.h>
struct stat st2;
if (stat("data.txt", &st2) == 0) {
printf("size=%ld mtime=%ld\n", (long)st2.st_size, (long)st2.st_mtime);
}
// tmpfile(): unnamed temp file, auto-deleted on close
FILE *t = tmpfile();
fprintf(t, "scratch\n");
rewind(t);
fclose(t);

13.常见坑

十个经典 C 坑 —— 正确写法标 GOOD,错误写法标 BAD。

缓冲区溢出

写过一个固定缓冲区的末尾会破坏相邻内存,是顶级安全漏洞。给每处写入加上限:strncpy 手补 NUL、snprintf、或带长度的 fgets。Sanitizer 能抓到人眼漏掉的问题。

1
2
3
4
5
char buf[10];
strcpy(buf, src); // BAD — writes past buf if src is long
strncpy(buf, src, sizeof buf - 1);
buf[sizeof buf - 1] = '\0'; // GOOD — bounded, NUL-terminated
snprintf(buf, sizeof buf, "%s", src); // GOOD — simplest

释放后使用 / 双重释放

释放后使用是未定义行为,往往可被利用;同一块内存释放两次会破坏分配器。free 后立刻把指针置 NULL——free(NULL) 是安全的空操作。

1
2
3
4
5
int *p = malloc(sizeof(int));
free(p);
*p = 42; // BAD — dangling pointer (UB)
p = NULL; // GOOD — marks it freed
free(p); // safe (free(NULL) is a no-op)

内存泄漏

每块分配都必须在每条路径上释放;泄漏静默膨胀,直到进程死亡。AddressSanitizer 和 valgrind 默认就会报告——开发期运行,而不是崩溃之后。

1
2
3
int *p = malloc(4 * sizeof(int));
return; // BAD — leaks
free(p); // GOOD — before every return

= 与 ==

单个 = 是赋值;== 是比较。if (x = 0) 会赋零且永远为假——一个无声又难发现的 bug。用 -Wall 编译,条件里的赋值要加括号。

1
2
3
if (x = 0) { /* ... */ } // BAD — assigns, condition is false
if (x == 0) { /* ... */ } // GOOD
if ((x = foo()) != 0) { /* ... */ } // GOOD — assign in condition with parens

有符号溢出是 UB

有符号整数溢出是未定义行为:编译器可能假定它永远不会发生并据此优化。加大的有符号值之前先扩成 long long,或者就用你本意的无符号回绕。

1
2
3
int a = 2000000000, b = 2000000000;
int c = a + b; // BAD — signed overflow = UB
long long c2 = (long long)a + b; // GOOD — widen first

未初始化变量

读未初始化的局部变量是未定义行为,通常拿到垃圾值。在声明处初始化每个变量——编译器 -Wuninitialized 才有真东西可查。

1
2
3
int x;
printf("%d\n", x); // BAD — indeterminate value
int x = 0; // GOOD — always initialize

sizeof 中的数组退化

函数内部的数组参数是指针,sizeof 得到 8(指针大小)而非数组大小。sizeof a / sizeof a[0] 只在真正的数组仍在作用域内时有效——显式传长度。

1
2
3
4
5
void f(int arr[10]) {
sizeof(arr); // BAD — size of pointer (8), not 40
}
// GOOD — pass the length explicitly:
void g(const int *arr, size_t n);

差一错误

循环边界是经典的差一错误:n 个元素要迭代 i < n(下标 0..n-1),而不是 i <= n 那样多碰一个越界。大小 n 的数组最后一个合法下标是 n-1。

1
2
for (int i = 0; i <= n; i++) // BAD — runs n+1 times
for (int i = 0; i < n; i++) // GOOD

printf 格式不匹配

格式符与实参类型不匹配是未定义行为——用 %s 配 int 可能直接崩溃。size_t 用 %zu,%d/%ld/%lld 要对上类型,开 -Wformat 抓不匹配。

1
2
3
printf("%s\n", x); // BAD — %s expects char*, x is int
printf("%d\n", x); // GOOD
printf("%zu\n", sizeof x); // GOOD — size_t needs %zu

有符号 / 无符号比较

有符号与无符号比较时,有符号操作数被提升为无符号,负数变成巨大的正数。把两侧都显式转成同一有符号类型,或者干脆别混用。

1
2
3
4
5
int i = -1;
unsigned u = 0;
if (i < u) { /* ... */ } // BAD — i promoted to unsigned, huge
if ((long long)i < (long long)u) { /* */ } // GOOD — compare as signed
// Also: (int)1 < (unsigned)-1 is FALSE — surprising, use explicit casts

14.线程与并发

pthread create/join、互斥锁、条件变量、原子操作与 C11 线程 —— 构建与链接加 -pthread。

构建与头文件

线程程序用 -pthread 编译,它链接 pthread 库。包含 <pthread.h>,并把每个 pthread_* 的返回值当成要检查的错误——这些 API 不设置 errno。

1
2
3
// Build: $ gcc -pthread prog.c -o prog
#include <pthread.h>
#include <stdio.h>

创建与等待

pthread_create 启动一个线程运行某个函数,该函数接收一个 void* 参数;pthread_join 阻塞等待它结束。传入的参数和传回的 void* 就是跨线程传递数据的通道。

1
2
3
4
5
6
7
8
9
void *worker(void *arg) {
int id = *(int *)arg;
printf("thread %d\n", id);
return NULL;
}
pthread_t t;
int id = 1;
pthread_create(&t, NULL, worker, &id);
pthread_join(t, NULL); // wait for t

通过 void* 传出数据

线程把结果以 void* 返回——通常是指向 malloc 出来的值的指针。调用者 join 后把返回的指针强转回来、读出值、再释放。

1
2
3
4
5
6
7
8
9
10
11
void *compute(void *arg) {
int *out = malloc(sizeof(int));
*out = 42;
return out;
}
pthread_t t2;
pthread_create(&t2, NULL, compute, NULL);
void *res;
pthread_join(t2, &res);
int value = *(int *)res; // 42
free(res);

互斥锁——保护共享状态

互斥锁串行化对共享状态的访问:访问前加锁,之后解锁。临界区保持短小——锁是瓶颈,长时间持有还会引来死锁。

1
2
3
4
5
6
7
8
9
10
11
12
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
long shared = 0;
void *inc(void *arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&mtx);
shared++;
pthread_mutex_unlock(&mtx);
}
return NULL;
}
// init at runtime with attributes:
// pthread_mutex_init(&mtx, NULL);

条件变量——等待 / 通知

条件变量让线程睡下,直到另一个线程通知某个谓词改变了。永远在 while 循环里等待(有虚假唤醒),并且要持有互斥锁,醒来后重新检查谓词。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pthread_mutex_t cv_m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
int ready = 0;
// Producer:
void *produce(void *a) {
pthread_mutex_lock(&cv_m);
ready = 1;
pthread_cond_signal(&cv); // wake one waiter
pthread_mutex_unlock(&cv_m);
return NULL;
}
// Consumer:
void *consume(void *a) {
pthread_mutex_lock(&cv_m);
while (!ready) // loop — spurious wakeups
pthread_cond_wait(&cv, &cv_m);
pthread_mutex_unlock(&cv_m);
return NULL;
}

分离线程——无需 join

分离线程结束时会自行清理,无需 join。只在确定永远不会等它的时候才分离——也绝不要把指向调用者栈的指针传给分离线程,那时栈可能已经没了。

1
2
3
4
5
6
pthread_t t3;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&t3, &attr, worker, NULL);
pthread_attr_destroy(&attr);

C11 线程(可移植)

C11 的 <threads.h>(thrd_t)是比 pthreads 更薄、可移植的替代——能力更弱,常常只是对 OS API 的薄封装。POSIX 上做正经事,pthreads 仍是标准选择。

1
2
3
4
5
// #include <threads.h>
// int run(void *a) { return 0; }
// thrd_t th;
// thrd_create(&th, run, NULL);
// thrd_join(th, NULL);

原子操作(C11)——<stdatomic.h>

atomic_int 和 atomic_fetch_add 提供有定义内存顺序的无锁加载、存储和自增——比互斥锁快的简单共享计数器,也比普通 int 更正确(不会竞争)。

1
2
3
4
5
#include <stdatomic.h>
atomic_int counter = 0;
atomic_fetch_add(&counter, 1);
int now = atomic_load(&counter);
// Lock-free check: atomic_is_lock_free(&counter)

线程局部存储(C11)

_Thread_local(C11,GCC 里是 __thread)让每个线程拥有自己的变量副本。它适合每线程缓存,也适合不得互相覆盖的错误槽。

1
2
3
#include <threads.h>
_Thread_local int tls_count = 0; // each thread its own copy
// GCC/Clang also: __thread

线程中的错误处理

pthread 函数返回 errno 风格的错误码而不是设置 errno。把返回值接住并用 strerror(rc) 格式化诊断——不检查的失败通常表现为神秘的挂起或崩溃。

1
2
3
4
5
int rc = pthread_create(&t, NULL, worker, NULL);
if (rc != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(rc));
}
// pthread functions return an errno-style code (not set errno)

死锁规避

死锁来自“持有所需之物还等待别处所需”。按固定的全局顺序加锁、让临界区尽量小、优先一资源一锁,需要有界等待时用带超时的锁。

1
2
3
// Lock in a fixed global order.
// Try pthread_mutex_timedlock for bounded waits.
// Prefer one lock per resource; keep critical sections short.

15.网络(Socket)

getaddrinfo、TCP 客户端与服务端、最小 HTTP 请求 —— Linux/macOS 下无需额外库链接。

构建与头文件

POSIX 套接字需要 <sys/socket.h>、<netinet/in.h> 和 <arpa/inet.h>;Linux/macOS 无需额外链接库。用 getaddrinfo 解析地址,不要手填 sockaddr 结构。

1
2
3
4
5
6
// Build: $ gcc -std=c17 prog.c -o prog (Linux/macOS; Windows uses Winsock)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>

TCP 客户端——解析、连接、收发

TCP 客户端用 getaddrinfo 解析服务器、创建套接字、再 connect。地址列表可能很长——逐个尝试,用第一个成功的,失败的随手关掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int connect_tcp(const char *host, const char *port) {
struct addrinfo hints = {0}, *res;
hints.ai_family = AF_UNSPEC; // IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP
int g = getaddrinfo(host, port, &hints, &res);
if (g != 0) { fprintf(stderr, "%s\n", gai_strerror(g)); return -1; }
int fd = -1;
for (struct addrinfo *ai = res; ai; ai = ai->ai_next) {
fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (fd < 0) continue;
if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) break;
close(fd); fd = -1;
}
freeaddrinfo(res);
return fd;
}

发送 / 接收

send/recv 在已连接的套接字上搬运字节,但都不保证一次搬完。循环直到收满整条消息或遇到 EOF,一路跟踪还剩多少字节。

1
2
3
4
5
6
7
int fd = connect_tcp("example.com", "80");
const char *req = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n";
send(fd, req, strlen(req), 0);
char buf[4096];
ssize_t n = recv(fd, buf, sizeof buf - 1, 0); // may be partial
buf[n] = '\0';
close(fd);

TCP 服务器——socket 流程

服务器创建套接字、设 SO_REUSEADDR、bind 到端口、listen,再循环 accept 连接。每次 accept 返回一个专属于该客户端的新套接字——监听套接字从不被读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int srv = socket(AF_INET, SOCK_STREAM, 0);
int one = 1;
setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(8080) };
addr.sin_addr.s_addr = INADDR_ANY;
bind(srv, (struct sockaddr *)&addr, sizeof addr);
listen(srv, 16);
for (;;) {
int cfd = accept(srv, NULL, NULL);
char req[1024];
recv(cfd, req, sizeof req - 1, 0);
const char *resp = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
send(cfd, resp, strlen(resp), 0);
close(cfd);
}

解析主机名 -> IP

getaddrinfo 把主机名和端口变成一列候选地址,顺带处理 IPv4/IPv6 和服务名。逐个试到成功为止,然后用 freeaddrinfo 释放。

1
2
3
4
5
6
struct addrinfo *r;
getaddrinfo("example.com", NULL, &(struct addrinfo){.ai_family=AF_INET}, &r);
struct sockaddr_in *sa = (struct sockaddr_in *)r->ai_addr;
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &sa->sin_addr, ip, sizeof ip);
freeaddrinfo(r);

阻塞 vs 非阻塞

套接字默认阻塞:recv 一直等到数据来。O_NONBLOCK 让调用在没准备好时返回 EAGAIN,poll/epoll/select 就能在单线程里监管大量套接字。

1
2
3
// fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
// Non-blocking recv returns -1 with errno==EAGAIN/EWOULDBLOCK when idle.
// For many clients: poll()/select()/epoll (Linux) — see man poll.

UDP——sendto/recvfrom(无连接)

UDP 无连接:sendto/recvfrom 每次都要带上目标地址,没有握手、没有顺序、没有投递保证。适合对延迟敏感而容忍丢失的流量。

1
2
3
int us = socket(AF_INET, SOCK_DGRAM, 0);
sendto(us, "ping", 4, 0, (struct sockaddr *)&addr, sizeof addr);
recvfrom(us, buf, sizeof buf, 0, NULL, NULL);

字节序——htons/htonl 与网络序

网络字节序是大端;宿主机可能是小端。填 sockaddr 字段、以及从线路上读数字协议字段时,用 htons/htonl/ntohs/ntohl 转换。

1
2
// uint16_t port = htons(8080); host -> network
// uint16_t hp = ntohs(port); network -> host

Windows(Winsock)的区别:

Windows 用 winsock2.h,使用套接字前要先调一次 WSAStartup,用 closesocket() 而不是 close() 关套接字。其余 API 与 POSIX 套接字一致,代码大体可移植。

1
2
3
// #include <winsock2.h> + ws2_32.lib
// WSAStartup(MAKEWORD(2,2), &wsaData); ... WSACleanup();
// close() -> closesocket()

错误码

套接字调用失败返回 -1 并置 errno——ECONNREFUSED、ETIMEDOUT、ECONNRESET——getaddrinfo 则返回自己的错误码给 gai_strerror。每一步都检查;这里的静默失败会变成之后的挂起。

1
2
3
// socket() -> -1 + errno
// connect() -> -1 + errno (ECONNREFUSED, ETIMEDOUT, EHOSTUNREACH)
// getaddrinfo() -> gai_strerror(rc)

HTTPS 需要 TLS 库

裸套接字只给你 TCP。加密需要 TLS 库——OpenSSL 的话先建 SSL_CTX,用 SSL_new/SSL_connect 包住套接字,再用 SSL_read/SSL_write——或者用 libcurl 走高层 HTTP+TLS API。

1
// Raw sockets only give you TCP. See libcurl for a higher-level API.

16.时间与日期

墙上时钟时间、UTC/本地转换、strftime 格式化与高精度单调计时。

Unix 纪元秒

time(NULL) 返回自 1970-01-01 00:00 UTC 以来的整秒数(time_t)——存储时间戳用的无时区、普适的瞬间。

1
2
3
4
#include <time.h>
#include <stdio.h>
time_t now = time(NULL); // seconds since 1970-01-01 UTC
printf("%lld\n", (long long)now);

转换为 UTC / 本地 struct tm

gmtime 和 localtime 把 time_t 拆成 struct tm(年、月、日、时……)——分别是 UTC 和本地时区。注意偏移:tm_year 是自 1900 起的年数,tm_mon 是 0-11。

1
2
3
4
5
struct tm *utc = gmtime(&now); // UTC
struct tm *loc = localtime(&now); // local timezone
int year = utc->tm_year + 1900; // tm_year is years since 1900
int mon = utc->tm_mon + 1; // tm_mon is 0-11
int day = utc->tm_mday;

用 strftime 格式化

strftime 用 %Y %m %d %H:%M:%S 等把 struct tm 格式化成可读文本——日期界的 printf。它是拼日志行和人类可读时间戳的标准方式。

1
2
3
4
5
char out[64];
strftime(out, sizeof out, "%Y-%m-%d %H:%M:%S", loc);
printf("%s\n", out); // 2026-08-02 14:05:09
// %Y year %m month %d day %H hour %M minute %S second
// %a weekday %A full name %z timezone offset %s epoch

把日期解析回 time_t

strptime(POSIX)把文本解析进 struct tm,mktime 再转成 time_t。把 tm_isdst 设为 -1,让 mktime 自己处理夏令时而不是猜错。

1
2
3
4
struct tm t = {0};
strptime("2026-08-02", "%Y-%m-%d", &t); // POSIX
// t.tm_isdst = -1; // let mktime resolve DST
// time_t epoch = mktime(&t);

已过墙钟时间

difftime(b, a) 返回两个 time_t 之间的墙钟秒数。它简单,对粗粒度时长够用,但墙钟会跳——做测量要用单调时钟。

1
2
3
4
time_t a = time(NULL);
/* work */
time_t b = time(NULL);
printf("%.0f s\n", difftime(b, a)); // difftime returns double

高精度单调时钟

clock_gettime(CLOCK_MONOTONIC) 测量的流逝时间不受 NTP 和夏令时调校影响——基准测试、超时、任何不许跳的计时都用它。

1
2
3
4
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts); // immune to wall-clock changes
long ms = ts.tv_sec * 1000L + ts.tv_nsec / 1000000L;
// CLOCK_MONOTONIC: elapsed time. CLOCK_REALTIME: wall clock.

纳秒级睡眠(C11 <time.h>)

nanosleep 以 timespec 精度(纳秒级)挂起线程。被信号打断时会报告剩余时间,好让调用继续睡完。

1
2
struct timespec req = { .tv_sec = 0, .tv_nsec = 5000000 }; // 5 ms
nanosleep(&req, NULL);

本进程消耗的 CPU 时间

clock() 返回本进程消耗的 CPU 时间,不是墙钟时间。对比 CPU 与墙钟时间,能看出任务在等 I/O 而非在计算。

1
2
3
clock_t c = clock(); // CPU clock ticks
/* work */
printf("%.2f s\n", (double)(clock() - c) / CLOCKS_PER_SEC);

获取时区偏移

tm_gmtoff(GNU/glibc)保存本地时间相对 UTC 的秒数(东为正),让你不用猜就能渲染 RFC 822 风格的 +0800 偏移。

1
2
struct tm *l = localtime(&now);
printf("%ld\n", l->tm_gmtoff); // seconds east of UTC (GNU)

不用 strftime 输出 ISO 8601

用 snprintf 按 UTC struct tm 的字段拼出 ISO 8601 时间戳。输出与语言环境无关、无歧义——接口和日志的理想选择。

1
2
3
4
char iso[32];
snprintf(iso, sizeof iso, "%04d-%02d-%02dT%02d:%02d:%02dZ",
utc->tm_year + 1900, utc->tm_mon + 1, utc->tm_mday,
utc->tm_hour, utc->tm_min, utc->tm_sec);

17.进程与信号

fork、exec、wait、进程间管道、信号处理与最小守护进程。

平台与头文件

fork/exec/wait 和信号是 POSIX(Linux/macOS);Windows 用 CreateProcess,模型不同。本节调用需要包含 <unistd.h>、<sys/wait.h> 和 <signal.h>。

1
2
3
4
5
// POSIX only (Linux/macOS). Build: $ gcc prog.c -o prog
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdio.h>

fork——克隆当前进程

fork 克隆当前进程。两份副本从同一返回点继续:子进程看到 pid 0,父进程看到子进程的 pid。子进程应当 _exit()——从 main 返回会跑父进程的清理并重复冲刷缓冲区。

1
2
3
4
5
6
7
8
9
10
11
12
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
// child — this code runs only in the child
printf("child: pid=%d\n", getpid());
_exit(0); // child should _exit, not return
} else {
// parent
int status;
waitpid(pid, &status, 0); // block until child exits
if (WIFEXITED(status)) printf("exit=%d\n", WEXITSTATUS(status));
}

exec——替换进程映像

exec 用另一个程序替换当前进程映像:pid 不变,代码全新。它只在失败时返回(随后调用 _exit(127),shell 惯例)。环境、文件描述符和工作目录都会保留。

1
2
3
4
5
6
if (pid == 0) {
char *argv[] = { "ls", "-l", NULL };
execvp(argv[0], argv); // search PATH
perror("execvp");
_exit(127); // only reached on failure
}

fork/exec/wait 运行命令

运行外部命令就是 shell 自己的那套:fork(克隆)、execvp(换映像)、waitpid(回收)。WEXITSTATUS 取出子进程的退出码,WIFSIGNALED 告诉你它是否被信号杀死。

1
2
3
4
5
6
7
int run(const char *cmd, char *const args[]) {
pid_t pid = fork();
if (pid == 0) { execvp(cmd, args); _exit(127); }
int status;
waitpid(pid, &status, 0);
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

管道——子进程写、父进程读

pipe(fd) 创建一对相连的描述符:一端写、一端读。fork 之后各进程关掉自己不用的那一端,读端在写端关闭时就看到 EOF。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int fd[2];
pipe(fd);
pid_t p = fork();
if (p == 0) {
close(fd[0]); // child: close read end
write(fd[1], "hello\n", 6);
close(fd[1]);
_exit(0);
}
close(fd[1]); // parent: close write end
char buf[128];
ssize_t n = read(fd[0], buf, sizeof buf - 1);
buf[n] = '\0';
close(fd[0]);
waitpid(p, NULL, 0);

信号处理器

signal() 为异步信号注册处理器。处理器内部只能调用 async-signal-safe 函数——write、_exit、signal、kill——绝不能 printf 或 malloc,它们不可重入。

1
2
3
4
5
6
7
void on_sigint(int sig) {
write(2, "interrupted\n", 12); // async-signal-safe only
_exit(130);
}
signal(SIGINT, on_sigint);
// Safe functions in a handler: write, _exit, signal, kill
// NOT safe: printf, malloc, most libc — use sigaction + sigqueue for real code.

sigaction——健壮的 API

sigaction 是 signal() 的健壮替代:能在处理器运行期间屏蔽其他信号、设置 SA_RESTART 恢复被中断的系统调用、并让处理器知道是哪个信号触发的。新代码优先用它。

1
2
3
4
5
struct sigaction sa = {0};
sa.sa_handler = on_sigint;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGTERM, &sa, NULL);

临时屏蔽信号

sigprocmask 在临界区周围临时推迟信号、之后再解除,处理器就不会在共享状态改了一半时插入。配合一个处理“保存下来的事件”的处理器,信号用法才安全。

1
2
3
4
5
6
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, NULL);
/* critical section */
sigprocmask(SIG_UNBLOCK, &set, NULL);

杀死进程 / 检查存在性

kill(pid, sig) 给进程发信号;kill(pid, 0) 只检查进程存在且你有权发信号,不真的送达。负数 pid 针对整个进程组。

1
2
kill(pid, SIGTERM); // send signal to pid
// kill(pid, 0) == 0 -> process exists (may still be zombie)

僵尸进程——回收子进程

先于父进程 wait 就退出的子进程会变成僵尸,占着一个 pid 直到被 wait/waitpid 回收。忽略 SIGCHLD 让子进程自动被回收,或在处理器里显式回收。

1
2
3
4
// Child that exits before parent waits becomes a zombie (shows as Z in ps).
// Reap via wait/waitpid, or install a SIGCHLD handler:
signal(SIGCHLD, SIG_IGN); // simple: auto-reap
// Or in the handler: while (waitpid(-1, NULL, WNOHANG) > 0) {}

守护进程骨架(双重 fork)

守护进程脱离控制终端:先 fork 让子进程不是进程组组长,再 setsid 开启新会话,再 fork 一次,chdir('/'),并把 stdio 重定向到 /dev/null 或日志。

1
2
3
4
5
// if (fork() != 0) _exit(0); // 1st fork
// setsid(); // new session, detach tty
// if (fork() != 0) _exit(0); // 2nd fork — never session leader
// chdir("/"); umask(0);
// redirect stdin/out/err to /dev/null or a log file

18.POSIX 正则表达式

regcomp/regexec/regerror:编译模式、匹配与提取捕获组、遍历所有匹配。

设置与头文件

POSIX 正则(regcomp/regexec)内建于 Linux/macOS 的 libc,无需额外库。传 REG_EXTENDED 时使用 ERE 语法——默认的 BRE 是另一套更老的方言。

1
2
3
// POSIX regex (<regex.h>), Linux/macOS.
#include <regex.h>
#include <stdio.h>

编译与匹配

先用 regcomp 编译一次模式(并检查返回值!),再用 regexec 对字符串匹配。用 regfree 释放编译好的模式——每个 regcomp 都配一个 regfree。

1
2
3
4
5
6
7
8
9
10
11
12
regex_t re;
int rc = regcomp(&re, "^[a-z]+[0-9]+$", REG_EXTENDED);
if (rc != 0) { // REG_EXTENDED = modern ERE syntax
char err[128];
regerror(rc, &re, err, sizeof err);
fprintf(stderr, "regex: %s\n", err);
return 1;
}
rc = regexec(&re, "abc123", 0, NULL, 0);
if (rc == 0) puts("matched");
else if (rc == REG_NOMATCH) puts("no match");
regfree(&re); // always release

捕获组

括号分组会落入 regmatch_t 条目:m[1] 保存第 1 组在匹配文本中的起止偏移。用 %.*s 精度技巧配合偏移对打印捕获片段。

1
2
3
4
5
6
7
8
9
10
regmatch_t m[3]; // m[0]=whole, m[1]=group1, m[2]=group2
regex_t r2;
regcomp(&r2, "(\\w+)@(\\w+)\\.(\\w+)", REG_EXTENDED);
const char *text = "mail [email protected] now";
if (regexec(&r2, text, 3, m, 0) == 0) {
// m[i].rm_so/rm_eo are offsets into 'text'
printf("user: %.*s\n",
(int)(m[1].rm_eo - m[1].rm_so), text + m[1].rm_so);
}
regfree(&r2);

遍历所有匹配(用 REG_NOTBOL 循环)

要找全部匹配,就在字符串尾部循环 regexec,每次越过一个匹配。第一次迭代之后传 REG_NOTBOL,避免 ^ 在字符串中途重新锚定。

1
2
3
4
5
6
7
8
9
10
regex_t r3;
regcomp(&r3, "\\d+", REG_EXTENDED);
const char *s = "a1 b22 c333";
regmatch_t match;
int off = 0;
while (regexec(&r3, s + off, 1, &match, off ? REG_NOTBOL : 0) == 0) {
printf("num: %.*s\n", (int)(match.rm_eo - match.rm_so), s + off + match.rm_so);
off += match.rm_eo; // advance past the match
}
regfree(&r3);

常用标志

REG_EXTENDED 开启 ERE 语法(用它);REG_ICASE 大小写不敏感匹配;REG_NOSUB 跳过捕获跟踪换取提速;REG_NEWLINE 让 ^/$ 匹配行首行尾而不是整个字符串。

1
2
3
4
5
// REG_EXTENDED: ERE syntax (+, ?, |, (), {m,n})
// REG_ICASE: case-insensitive matching
// REG_NOSUB: skip capture info, faster
// REG_NEWLINE: '.' won't cross newlines; ^/$ anchor per line
// regexec(..., REG_NOTBOL): next char is not a line start

ERE 语法速查

ERE 速查:. 任意字符,^ $ 锚点,[] 字符类,* + ? 重复,{m,n} 有界重复,(a|b) 交替,反向引用,以及括号内的 POSIX 类如 [[:alpha:]]。

1
2
3
4
// . any char ^ $ anchors [abc] [a-z] [^0-9] classes
// * + ? {2,4} repetition
// (a|b) groups \\1 backref \\d \\w \\s (GNU)
// POSIX classes: [[:alpha:]] [[:digit:]] [[:space:]] [[:alnum:]]

替换——没有内建;手动拼输出

POSIX 正则没有内建替换。拼输出:循环匹配,把匹配之间的文本复制进来,在每个匹配处插入替换串——和遍历全部匹配的循环是同一个套路。

1
2
3
// Step 1: regexec to find each match
// Step 2: copy text up to match, append replacement, continue
// Step 3: copy the tail. See regexec iteration in §3.

转义用户输入

要把用户输入嵌进模式里,先给每个元字符([\\^$.|?*+()[])加上反斜杠转义。否则精心构造的输入会改变你模式的含义——这是注入漏洞,不只是逻辑问题。

1
2
// Prefix every char in [\\^$.|?*+()[]{} with '\\' before regcomp
// to treat the input literally.

检查 regcomp 并 regfree

regcomp 遇到坏模式会失败——务必检查返回值并用 regerror 格式化。而且要每个 regcomp 配一个 regfree,否则循环里反复匹配会泄漏编译好的模式。

1
2
// regcomp can fail (bad pattern) — see §1 error path.
// Every regcomp must pair with a regfree.

局限与性能

复用编译好的 regex_t:每匹配一次都编译是浪费。POSIX 正则会回溯,病态模式在不可信输入上可能很慢——重度或对抗性使用,优先 PCRE2 或 RE2。

1
2
3
// regcomp/regexec are compiled once; reuse the regex_t.
// Patterns are backtracking-based; pathological input can be slow
// (quadratic). For heavy use prefer PCRE2 or RE2.

编译期匹配,实现 switch 式分派

启动时预编译若干模式,按顺序链式调用 regexec。当输入可能匹配多种形状时,它读起来像一张清晰的路由表——比一墙字符串比较强。

1
2
3
// if (regexec(&re_ipv4, s, 0, NULL, 0) == 0) {}
// else if (regexec(&re_ipv6, s, 0, NULL, 0) == 0) {}
// Pre-compile all patterns at startup, reuse them.

19.构建与调试

编译器标志、最小 Makefile、gdb、消毒器与 valgrind —— 找出 FAQ 章节所警告 bug 的工具。

常用 gcc/clang 标志

-std=c17 固定语言,-Wall -Wextra 开启有用警告(一直开),-g 加调试信息,-O0/-O2 选优化级别,-fsanitize=address,undefined 加运行时检查。零成本还能抓到真 bug。

1
2
3
4
5
6
7
8
9
10
// -std=c17: language standard
// -Wall -Wextra: enable warnings (always)
// -Werror: warnings become errors (CI)
// -pedantic: reject non-ISO extensions
// -g: debug info (needed for gdb / asan traces)
// -O0 / -O1 / -O2: optimization levels
// -fsanitize=address,undefined: runtime checkers
// -o out: output name -I include dir
// -L libdir -lm: link libm -pthread
$ gcc -std=c17 -Wall -Wextra -g -O0 prog.c -o prog

Makefile——极简构建驱动

极简 Makefile 把编译器、标志、目标名和对象列表放进变量,依靠隐式规则 %.o:%.c,再提供一个 clean 目标。make 只重建有变化的部分——这正是它的意义。

1
2
3
4
5
6
7
8
9
10
11
// CC = gcc
// CFLAGS = -std=c17 -Wall -Wextra -g
// TARGET = app
// OBJS = main.o utils.o
// $(TARGET): $(OBJS)
// \t$(CC) $(CFLAGS) -o $@ $(OBJS)
// %.o: %.c
// \t$(CC) $(CFLAGS) -c $<
// clean:
// \trm -f $(TARGET) $(OBJS)
// $ make && ./app

调试构建 + 发布构建

调试构建用 -g -O0 保留完整符号、不优化;发布构建用 -O2 -DNDEBUG,顺带把 assert 编译掉。两个目标都留着,才能在用户跑的确切配置下复现崩溃。

1
2
3
// Debug: gcc -g -O0 -Wall -Wextra
// Release: gcc -O2 -DNDEBUG (asserts compiled out)
// Profile: gcc -O2 -pg + gprof ./app

gdb 基础

gdb ./prog,然后:break main、run、next/step 逐行、print x 查看、backtrace 看调用栈、continue、quit。配合 -batch -ex 还能非交互地给 CI 生成崩溃转储。

1
2
3
4
5
6
7
8
9
10
11
// $ gcc -g prog.c -o prog
// $ gdb ./prog
// (gdb) break main set breakpoint
// (gdb) run start
// (gdb) next / step line / into
// (gdb) print x inspect variable
// (gdb) backtrace call stack (bt)
// (gdb) list show source
// (gdb) continue resume (c)
// (gdb) quit
// Non-interactive: $ gdb -batch -ex 'run' -ex 'bt' ./prog

AddressSanitizer(ASan)

编译时加 -fsanitize=address,undefined,程序就会在运行时带着栈回溯精确报告溢出、释放后使用和泄漏。比 valgrind 快也清晰——第一个要上的工具。

1
2
3
// $ gcc -fsanitize=address,undefined -g prog.c -o prog
// $ ./prog -> detailed report on crash (use-after-free, overflow)
// Leak detection: $ ASAN_OPTIONS=detect_leaks=1 ./prog

valgrind 内存检测

valgrind --leak-check=full ./prog 无需重编译就能报告非法读写、释放后使用和确定泄漏——但慢约 20-50 倍。在无法重编译或需要更多细节时使用。

1
2
3
4
// $ gcc -g prog.c -o prog
// $ valgrind --leak-check=full ./prog
// Reports: invalid reads/writes, use-after-free, definite leaks.
// Slower (~20-50x) — use on failure, not in production.

UBSan 标志

-fsanitize=undefined 在运行时用精确报告抓有符号溢出、未对齐访问等未定义行为。和 -fsanitize=address 组合,一次构建同时覆盖内存与 UB。

1
2
3
// -fsanitize=undefined detects: signed overflow, shift UB,
// misaligned access, null deref, etc.
// Combine: -fsanitize=address,undefined

编译到汇编 / 预处理

gcc -S 输出汇编,gcc -E 显示预处理后的源码,gcc -c 生成不链接的目标文件。每个都是一扇看编译器与预处理器到底做了什么的小窗。

1
2
3
// $ gcc -S prog.c -> prog.s (assembly)
// $ gcc -E prog.c -> preprocessed source
// $ gcc -O2 -c prog.c -> prog.o (object, no link)

静态分析(可选)

gcc -fanalyzer、clang --analyze 和 cppcheck 不用运行程序就能找 bug——空指针解引用、泄漏、未初始化使用。加进 CI 很便宜,还能抓到测试漏掉的错误类别。

1
2
3
// $ gcc -fanalyzer prog.c GCC analyzer (GCC 10+)
// $ clang --analyze prog.c Clang static analyzer
// cppcheck prog.c third-party

构建系统

Make 适合中小项目;CMake 生成 Make 或 Ninja 文件,跨平台可移植;Meson 更新也更快。单个文件的话,一个 Makefile 或 shell 别名就够。

1
2
3
4
// Make (above) — small/medium projects
// CMake — portable, generates Make/Ninja files
// Meson — faster, more modern; Meson + Ninja
// For single files, just a Makefile or even a one-line shell alias.
关于本速查

本页是 ISO C(C11/C17)的自包含速查手册,覆盖语言本身和标准库在系统编程中约 80% 的常见用法,外加真实项目中常用的并发、Socket、正则与构建工具。权威参考见 C11 标准(ISO/IEC 9899:2011)与 cppreference 的 C 章节。 19 个章节各自聚焦一个主题 —— 从你的第一个程序、指针,到字符串、内存所有权与常见坑。每章拆分为 6–10 个子主题,附 5–15 行简洁示例。代码块刻意短小自明,每块都有复制按钮,可直接粘贴到编译器。 全部内容在你的浏览器内运行 —— 不上传、不追踪。本页是 GuruToolkit 免费开发者工具集的一部分;这里的代码片段可自由使用,不提供任何担保。

版本 2.3.0