本工具使用的開源套件

本工具程式碼中捆綁了 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