C 速查 — 簡明參考
C(C11/C17)簡明自包含參考手冊:語法、指針、內存、字符串、結構體與文件 I/O,外加線程、Socket、正則、時間、進程與構建/調試要點。覆蓋約 80% 日常場景。
C C17 (ISO/IEC 9899:2018)
ISO C · 過程式 · 手動內存 · 靜態 · 弱類型(帶強制轉換)
建議學習路徑
C 新手?按此順序閲讀: 1. Hello World —— 編譯、運行、退出碼 2. 變量與常量 —— 類型、大小、const 3. 指針與數組 —— &、*、->、指針算術 4. 流程控制 —— if / switch / 循環 5. 函數與可變參數 —— 傳值 vs 傳指針 6. 字符串與轉換 —— printf 格式符表 需要時再查閲文件 I/O 與錯誤處理。熟練後再看結構體、面向對象風格模式及進階章節。 有具體任務?使用上方搜索框 —— 試試「malloc」「socket」「pthread」「qsort」或「regex」。
1.Hello World 與構建環境
編譯、運行並組織 C 程序:工具鏈、IDE 選擇、多文件、命令行參數、退出碼。
最小程序
每個 C 程序都從 main() 開始執行,並返回一個 int 狀態給操作系統:0 表示成功,其他值表示出錯。最上方的 #include 引入聲明,編譯器才知道 printf 是什麼。
編譯與運行(gcc / clang)
C 是編譯型語言而非解釋型:編譯器把源代碼翻譯成可執行文件,再運行它。始終開啓警告(-Wall -Wextra)並固定語言標準(-std=c17),讓可移植性問題在編譯期就暴露出來。
工具鏈與 IDE 清單
你需要編譯器加編輯器。常見組合:Linux 用 gcc,macOS 用 clang,Windows 用 MinGW-w64 或 MSVC,都可以在 VS Code 或 CLion 裏調用。先運行一次版本命令確認工具鏈可用,再去排查別的問題。
命令行參數
main 可以接收用户在命令行輸入的內容:argc 是參數個數,argv[] 是參數本身——argv[0] 永遠是程序名。把參數當字符串解析,需要數值時用 strtol/strtod 轉換。
多文件
大型程序拆成多個 .c 文件,配一個共享的 .h 頭文件放聲明。每個 .c 文件單獨編譯後鏈接;頭文件讓聲明與定義保持一致,簽名一改,所有用到它的地方都會同時暴露。
退出碼
main 返回的值會傳到 shell:0(EXIT_SUCCESS)表示成功,其他值表示失敗。腳本和 CI 都依賴這個約定,所以退出碼要有意義。Linux/macOS 用 $? 查看,Windows 用 %ERRORLEVEL%。
環境變量
getenv() 讀取進程環境裏的變量,返回其值,未設置時返回 NULL。它是不重新編譯就注入配置的標準做法——但要把讀到的值當作不可信輸入。
預處理器基礎
預處理器在編譯之前運行:#define 定義宏,# 和 ## 連接記號,#ifdef/#if 條件編譯代碼。它是純文本、無類型的一層——多數場合優先用真正的函數和 const。
2.變量與常量
聲明、類型大小、定寬整數、const 位置、存儲類、typedef 與枚舉。
基本聲明
聲明一個變量要先寫類型再寫名字,並在聲明處初始化(C99+)。聲明時初始化,避免後面讀到未初始化的垃圾值。
類型大小(64 位典型值)
C 只保證最小大小,int 在不同平台上可能是 16 位或 32 位。用 sizeof 打印目標平台的實際大小,保持誠實,絕不硬編碼字節數。
定寬類型(C99+,<stdint.h>)
當內存佈局必須精確——文件格式、網絡協議、哈希——就用 <stdint.h> 裏的 int32_t/uint64_t 等。它們只在平台確實提供時才存在,這正是精確寬度名字受青睞的原因。
無符號 / 有符號
無符號類型永不為負,按 2^N 取模迴繞;有符號類型可為負。兩者混在一個表達式裏時,有符號操作數會被提升為無符號——這是經典坑源(見 faq 節)。
const——只讀(編譯期檢查)
const 讓變量只讀,編譯器在構建期就能拒絕意外的寫入。凡是不得改變的東西都加上 const:它既説明意圖,也利於編譯器優化。
const 指針位置(經典易混點)
指針聲明裏 const 的位置決定保護誰:在 * 之前保護指向的值,在 * 之後保護指針本身。從右往左讀類型,就能看懂這兩行的差別。
存儲類別
存儲類別決定變量存在哪裏、活多久。auto 是局部變量的默認;static 加在文件作用域上讓名字不跨文件可見;static 加在局部變量上讓它在多次調用間保值;extern 引用別處定義的名字。
typedef——類型別名
typedef 給類型起新名字。它不是創建新類型,只是別名——但有了別名就能寫更短、更一致的類型,把 struct 和函數指針的噪音藏進一個可讀的詞。
enum——命名常量
enum 給一組相關的整數常量起名字。值從 0 開始逐個加 1,但也可以顯式賦值或指定範圍。枚舉自帶文檔屬性,還能用於 switch。
指定初始化(C99+)
C99 允許按名字初始化成員(.port = 8080)或按下標初始化數組元素([3] = 9),順序任意。沒提到的成員自動清零——比一長串位置初始化清晰得多。
複合字面量(C99+)
複合字面量用類似強轉的語法內聯構建一個臨時 struct/數組值。想給函數傳一次性值時,不用先聲明具名變量,很方便。
volatile(內存映射 I/O / 信號)
volatile 告訴編譯器某個變量可能在程序正常流程之外改變——硬件寄存器、信號處理器或另一個線程。它阻止編譯器對訪問做緩存或重排。
3.結構體、聯合體與聚合類型
結構體佈局、聯合體、位域、sizeof、_Static_assert 與對齊 —— 真實 C 數據的構建塊。
struct——異構記錄
struct 把不同類型但互相關聯的值歸成一個記錄。成員按聲明順序存放,中間會插入對齊填充;所以 sizeof(struct) 常常大於成員大小之和。
指向 struct 的指針——用 -> 訪問成員
箭頭 p->member 是 (*p).member 的簡寫,對結構體指針解引用訪問成員。按指針傳結構體很省(一個字),還能讓函數修改調用者的記錄。
匿名 struct + typedef
可以給沒有標籤的 struct 加 typedef,一條聲明就得到乾淨的類型名。這是給常用記錄類型定義名字的習慣做法,省得處處寫 struct。
union——重疊存儲(大小 = 最大成員)
union 把多個成員放進同一塊內存,大小取最大成員。只有最後寫入的那個成員才有效——讀其他成員是未定義行為,所以要配一個標籤字段。
位域——打包標誌位
位域把若干小標誌打包進一個整數的比特位,為含大量布爾值的記錄省內存。佈局由編譯器決定,所以位域只留在單個編譯單元內,不要跨 ABI 邊界。
sizeof——對象佔用的字節數
sizeof 給出類型或對象佔用的字節數。對仍在作用域內的數組,sizeof a / sizeof a[0] 可得到元素個數——但一旦數組退化為指針,這個技巧就悄悄失效(見 faq)。
_Static_assert(C11+)編譯期檢查
_Static_assert(cond, msg) 在編譯期斷言,cond 為假就構建失敗。它適合在尺寸和 ABI 假設被破壞的那一刻就校驗,而不是等到運行時才發現。
對齊——& _Alignof(C11+)
對齊決定對象可放在內存的哪個地址;編譯器給 struct 填充空隙讓每個成員自然對齊。_Alignof 報告對象的對齊值,_Alignas 在需要時提高它(SIMD、緩存行共享)。
柔性數組成員(C99+)——struct 尾部數組
struct 可以以一個不定長數組結尾(data[]),長度在分配時決定:一次 malloc 分配 struct 加額外字節。這是運行時才知道緩衝區大小的 C 慣用法。
枚舉作為獨立類型(C23)
C23 允許固定枚舉的底層類型(enum Status : unsigned char),大小不再由編譯器決定。否則枚舉與 int 兼容,這對多數代碼都夠用。
4.指針與數組
取地址、解引用、指針算術、數組衰減、指針 vs 數組、const 正確性。
取地址(&)與解引用(*)
& 取對象的地址得到指針;* 反向操作,通過指針讀寫。兩者配合讓你能間接訪問一個值,並在另一個作用域裏修改它。
NULL——空指針
NULL 是“什麼都不指向”的指針,不同於任何合法地址。解引用它是未定義行為,通常直接崩潰——所以用之前務必判斷是否為 NULL。
指針算術(按 sizeof(*p) 縮放)
指針加整數是按元素前進,不是按字節——p + 2 跳過兩個 int。下標 p[i] 其實就是 *(p + i)。越過數組末尾是未定義行為。
數組退化(表達式變指針)
多數表達式中,數組名變成指向首元素的指針。後果是:在函數內部無法從參數恢復數組長度——必須把長度和數組一起傳進來。
指針 vs 數組
char 數組自己擁有字節,可修改;指向字符串字面量的 char* 通常指向只讀內存。透過字面量指針寫入是未定義行為,常常崩潰。
多級指針
指向指針的指針(int**)多了一層間接。它用於字符串數組、二維結構,以及“由函數分配內存、調用者負責釋放”的輸出參數。
函數指針
函數指針存的是函數的地址,可以間接調用。它是回調、分派表,以及 oop 節裏那些面向對象模式的基石。
指針數組(如 argv)
char* 數組是存放字符串列表的慣用容器,argv 就是一個。每個元素指向各自以 NUL 結尾的字符串,數組本身只是一串指針。
const 正確性
把指針參數聲明為 const——例如 const char* s——等於承諾函數不會改動指向的內容。調用者就能放心傳字符串字面量和數組,編譯器會強制這份承諾。
數組指針 vs 指針數組
int *a[5] 是 5 個指針組成的數組;int (*b)[5] 是指向 5 個 int 數組的指針。括號改變一切。複雜聲明從右往左讀,或者用 cdecl 解碼。
5.流程控制
if/else、三元、switch、for、while、do-while、break/continue、goto 與 setjmp/longjmp。
if / else if / else
按條件分支:第一個條件為真的分支執行,否則走 else。else-if 鏈按順序測試多個備選——命中第一個即停。
三目運算符(表達式)
cond ? a : b 是表達式而非語句:按 cond 取 a 或 b。適合緊湊的取值選擇;如果一行放不下、讀不清,就改用 if/else。
switch——不 break 就穿透
switch 按整數值分派。除非 break,執行會穿透進下一個 case——要麼有意利用穿透(空 case 堆疊),要麼每個 case 都 break。default 處理未匹配的值。
for——在 init 中聲明計數器(C99+)
for(init; cond; step) 是標準計數循環。在 init 部分聲明計數器(C99+)能把它限定在循環內,避免和外層同名變量衝突。
while / do-while
while 每次迭代前測試條件,可能一次都不執行。do-while 總是先執行一次再測試——適合菜單和校驗提示這類“至少問一次”的場景。
break / continue
break 立即退出最內層循環或 switch;continue 跳到下一次迭代。用好了能攤平嵌套邏輯、去掉標誌變量;用壞了會隱藏控制流,所以儘量少層。
goto——跳出深層嵌套(少用、慎用)
goto 跳到某行標籤。它合理的用途是跳出深層嵌套、集中做錯誤清理——即 goto-cleanup 慣用法。除此之外,return 或標誌變量幾乎總是更清晰。
setjmp / longjmp 非局部跳轉
setjmp 保存執行上下文,longjmp 從任意位置恢復它,一路回捲到保存點。途中所有清理都被跳過——沒有析構函數會執行——所以比起 goto 或顯式 return,通常不划算。
循環慣用法
值得認識的循環形態:對計數範圍的下標循環、對數組的指針遍歷、以及帶 break 的無限循環。選最能直接表達意圖的那種。
邏輯短路求值
&& 和 || 從左到右求值,結果一確定就停止。這就是 ptr && ptr->x 安全的原因:第二個操作數只在第一個為真時才執行,所以可以在行內守衞解引用。
6.函數與可變參數
傳值 vs 傳指針、函數指針與 typedef、可變參數、遞歸與 _Generic。
按值傳遞——調用者的變量不變
C 所有參數都按值傳遞——被調函數拿到的是副本,修改它不影響調用者。簡單的標量和小的結構體這樣傳;需要修改或擔心大對象時用指針形式。
按指針傳遞(修改調用者)
要修改調用者的變量,就傳它的地址。被調函數解引用指針並寫入,改動就對調用者可見——scanf 這類函數正是這樣產生輸出的。
const 參數——只讀契約
const 參數如 const char* s 是隻讀契約:函數承諾不修改其指向的內容。它讓調用者能安全傳字符串字面量,也讓 API 意圖一目瞭然。
數組參數退化(需傳長度)
數組參數會退化為指針,長度不會跟着傳過去。務必顯式傳長度——否則函數無從知道數據在哪裏結束。
函數指針 + typedef
給函數指針簽名加一次 typedef,之後用別名聲明變量和參數。這樣分派表和回調讀起來不再是一堵括號加星號的牆。
可變參數——printf 風格函數
簽名帶 ... 的函數接收可變數量的參數,如 printf。用 va_start/va_arg/va_end 讀取;固定的參數必須説明後面還有多少,否則調用無法被安全解碼。
可變參數轉發——v* 系列函數
要把可變參數轉發給另一個可變參數函數(比如日誌封裝轉發給 vfprintf),用 v* 系列並把 va_list 本身傳過去。再用 va_arg 重讀會破壞列表。
遞歸
遞歸函數在更小的同類問題上調用自身,直到觸及基線。每次調用都佔棧空間,所以深度大或不可控時優先用迭代。
_Generic(C11+)——編譯期類型分派
_Generic(C11)根據控制表達式的類型,在編譯期選出結果表達式——一種輕量模擬函數重載、寫類型安全宏的辦法。
noreturn(C11+)與 inline
noreturn 標記永不返回的函數(如致命錯誤例程),編譯器因此能推斷不可達代碼。inline 只是請求而非保證——編譯器仍可能發出真實的調用。
7.字符串與數值轉換
NUL 結尾字符串、安全寫入、strtol/strtok/sscanf 轉換與 printf 格式符表。
基礎——以 NUL 結尾的字符數組
C 字符串是以 NUL 字節('\0')結尾的字符數組。strlen 數到終止符為止;任何裝字符串的緩衝區都得給它留位置,否則寫入就溢出。
複製 / 比較 / 拼接(不安全 vs 安全)
strcpy/strcat 不檢查邊界,源串太大時會靜默溢出。優先用 snprintf 做有界、總是以 NUL 結尾的寫入;strcmp/strncmp 比較;strncat 最多複製 n 個字符再加終止符。
查找
strchr 找字符首次出現,strrchr 找最後一次,strstr 找子串首次出現。都返回指向字符串內部的指針,找不到返回 NULL。
mem*——原始字節(可處理任意內存,含 NUL)
mem* 函數(memcpy、memmove、memset、memcmp)處理原始字節,能應對內嵌的 NUL。memcpy 和 memmove 的差別在於是否容忍重疊——可能重疊時用 memmove。
數值轉換:strtol/strtod
用 strtol/strtod 解析數值:它們會報錯並指出解析停在哪。atoi 遇到垃圾靜默返回 0,會掩蓋非法輸入——追求正確的代碼別用它。
健壯解析——校驗整個字符串
一次完整的解析要校驗三件事:strtol 後 errno 未置位、至少消耗了一位數字(end != s)、整個字符串都被讀完(*end == '\0')。跳過任何一條,尾部垃圾就溜進來了。
分詞:strtok_r 可重入
strtok 會原地修改字符串,在每個分隔符處插入 '\0';帶 _r 後綴的版本可重入(線程安全)。還需要原字符串的話,先複製一份。
sscanf——用格式串解析(返回匹配數)
sscanf 用格式串解析字符串,返回成功匹配的字段數。檢查這個返回值:比預期低,説明輸入不是你以為的形狀。
printf 格式表
printf 家族共用一套格式語言:%d int、%ld long、%zu size_t、%f double、%s 字符串、%p 指針,加上寬度和精度修飾符。格式符必須與實參類型精確匹配——不匹配是未定義行為。
安全地拼接字符串
snprintf 最多寫 n 字節且總是以 NUL 結尾,是安全拼字符串的辦法。對不斷增長的文字,記住長度並 realloc 緩衝區,或用一個小型動態緩衝輔助函數。
8.數組、排序與查找
數組遍歷、帶比較器的 qsort/bsearch、二維數組與結構體數組。
定長數組——帶長度迭代
C 數組是一塊固定大小的連續元素。用長度迭代:真正的數組仍在作用域內時 sizeof a / sizeof *a 有效,但一旦傳給函數,大小就丟了。
初始化模式
數組可按位置、按指定下標([2] = 9)、或整體清零({0})初始化。二維數組是數組的數組,行主序存放:m[i][j] 在每行內連續。
qsort——用比較器排序
qsort 用比較器原地排序,比較器返回 <0、0 或 >0。比較器收到的是指向元素的指針,所以先把 void* 參數轉回元素類型再安全比較(避免溢出)。
bsearch——在已排序數組上二分查找
bsearch 做二分查找——但只對已經排序的數組。它返回指向匹配元素的指針,找不到返回 NULL。大數組上先排序再查找,勝過線性掃描。
字符串比較器(char* 數組)
給 char* 數組排序必須比較字符串而不是指針值。比較器轉成 char* const*,解引用拿到每個字符串指針,再調用 strcmp。
結構體數組 + 按字段排序
要按某個字段給結構體數組排序,寫一個接收兩個結構體指針、比較該字段的比較器(比如 a->key 對 b->key 三路比較)。qsort 就會重排整個記錄。
數組指針參數(保持二維形狀)
把二維數組傳給函數,參數類型裏必須帶列數——int m[][COLS]——編譯器才能算出每行起點。第一維(行數)可以自由變化。
動態數組模式(另見 mem 節)
可增長集合是 malloc 出來的塊,滿了就 realloc:容量和長度分開記,增長時翻倍,用完 free。這是高層語言裏 vector/list 在 C 中的對應物。
變長數組(C99 可選)
變長數組按運行時值在棧上分配(int vla[n])。C11 中它是可選的,n 太大時可能撐爆棧——大的或需要可移植的東西優先用 malloc。
二維數組——行主序
int m[ROWS][COLS] 是連續的行主序塊;m[i][j] 就是 *(*(m + i) + j)。下標需要知道行長才能算出行起點,所以形狀必須在編譯期確定。
9.動態內存與所有權
malloc/calloc/realloc/free、棧 vs 堆、對齊、所有權規則與 goto cleanup 模式。
分配與釋放
malloc 返回未初始化的堆內存,每塊分配最終都要釋放。用前務必檢查是否為 NULL;釋放後把指針置 NULL,之後的重複釋放就成了安全的空操作。
calloc:count×size 清零
calloc(count, size) 會清零內存,並替你相乘兩個參數,還能檢測溢出。需要已初始化存儲且元素個數已知時優先用它。
realloc——調整大小;可能移動內存塊
realloc 調整分配的大小,可能把塊搬到新地址。先用臨時變量接收返回值並檢查:失敗時舊塊仍然有效,但如果你先覆蓋了指針,它就泄漏了。
棧 vs 堆
棧內存自動管理、快、返回時回收,但小且短命。堆內存手動管理、大、能活過函數。又大又長壽的緩衝區該放在堆上。
所有權——一組簡單規則
一組最少的所有權紀律能讓內存問題可控:誰分配誰釋放;每個指針只釋放一次;釋放後置 NULL;realloc 失敗時保留舊指針。約定就是 C 唯一的資源管理器。
堆上的字符串
堆上字符串需要 strlen(s) + 1 字節——多出來的是 NUL 終止符。malloc(strlen(s) + 1) 再複製;忘了 +1 就是一個待引爆的一字節溢出。
含內部指針的 struct
擁有堆數據的 struct 有兩塊分配要釋放:先內層緩衝區,再 struct 本身。決定並寫明誰擁有內部指針,按分配的逆序釋放。
goto cleanup(單一齣口)
C 沒有析構函數,所以 goto-cleanup 就是 C 的 RAII:每個函數一個出口、一個清理塊,所有路徑都到達它。每個資源在每條路徑上恰好釋放一次。
對齊 / 超對齊(C11)
aligned_alloc 能按指定對齊申請內存(SIMD、緩存行共享),大小必須是對齊值的整數倍。普通 malloc 只保證平台的默認對齊。
內存泄漏 / 溢出檢測工具(見 build 節)
泄漏、溢出和釋放後使用都藏到進程死掉才暴露——用 valgrind 或 AddressSanitizer 機械地找出來。兩者都在 build 節講過,是你能加的最便宜的安全網。
10.面向對象風格模式
C 是過程式語言,但結構體 + 函數指針可模擬類、繼承與多態。
“類” = struct + vtable
C 模擬類:用 struct 裝數據,用一張函數指針表(vtable)裝行為。方法就是帶顯式 self/this 指針參數的普通函數。
通過內嵌實現繼承
把基類 struct 內嵌為第一個成員就得到繼承:派生 struct 的地址等於基類的地址,所以派生值可以在任何期望基類的地方使用,基類方法原樣生效。
構造函數 / 析構函數配對
配一個負責分配並初始化的 init 函數和一個負責釋放並回收的 free 函數——C 版的構造/析構函數。每個被創建的對象都必須恰好銷燬一次。
不透明指針——隱藏實現(PIMPL)
頭文件裏只前置聲明 struct,定義只放在 .c 文件裏。調用者能持有指針但看不到內部——這就是 PIMPL,隱藏實現,讓內部改動不破壞調用者。
接口 = 函數指針組成的 struct
裝滿函數指針的 struct 就是接口:任何填充它的實現都能插進來,每個函數再收一個 void* self 作為實例上下文。這就是 C 形態的多態。
類型擦除數據(void *)
void* 抹掉類型,讓一個容器能裝異構的值。想安全地還原類型,通常得加一個標籤字段記錄指針真正指向什麼。
通過返回 *this 實現方法鏈
每個 setter 返回對象本身(*this),調用就能連起來:builder->setA(x)->setB(y)。它讓 builder 風格配置變得緊湊,代價是返回值可以被忽略。
單例(懶加載,線程不安全)
懶加載單例在首次使用時,把實例分配進一個 static 指針。這個簡單版本不是線程安全的——兩個線程可能競爭首次分配——一旦被共享就得加鎖。
引用計數(簡單版)
retain/release 給對象配一個引用計數,降到零就釋放。它是 Objective-C 的基礎,對 C 對象而言是比垃圾回收更輕的所有權模型。
完整 OOP 可行(glib)
C 裏存在完整的 OOP 框架——glib 和 GObject 就是證明——但它們帶來真實複雜度。最簡的 vtable 加不透明指針就能覆蓋多數需求;重武器留給真正需要的項目。
11.錯誤處理
無異常:errno、返回碼 + out 參數、goto cleanup、錯誤枚舉與斷言。
調用失敗後的 errno
庫函數和系統函數失敗時設置全局 errno,你必須在調用後立刻讀取或複製——任何後續調用都可能覆蓋它。strerror(errno) 把錯誤碼轉成可讀信息。
返回碼 + 出參(常見 C 慣用法)
C 主流錯誤慣用法:成功返回 0,失敗返回負錯誤碼,結果通過出參指針寫出。失敗時結果指針保持 NULL/不動,調用者可以據此判斷。
錯誤枚舉——顯式錯誤分類
枚舉你的函數可能產生的錯誤種類——OK、INVALID_INPUT、NOMEM、IO——並返回它們。具名錯誤碼自帶文檔、適合 switch,還逼你想清楚每種失敗模式。
assert——調試用(NDEBUG 移除)
assert(cond) 在 cond 為假時中止程序,開發期抓 bug。它在 -DNDEBUG 下被編譯掉,所以絕不能守衞用户依賴的行為——只校驗你假設為真的不變式。
perror——打印 errno 描述
perror 把你的消息連同當前 errno 的描述打印到 stderr 一行。這是報告系統調用失敗原因的最快方式——errno 部分不用自己寫格式串。
_Static_assert——編譯期
_Static_assert 在編譯期而不是運行期檢查假設。用它校驗尺寸、偏移和 ABI 約束,避免它們在生產二進制裏靜默失敗。
線程安全的 strerror_r(POSIX)
strerror 不是線程安全的,會覆寫自己的內部緩衝區。strerror_r(POSIX)改寫入調用者提供的緩衝區,併發線程各拿各的消息。
檢查系統調用返回值
系統調用會失敗——文件、套接字、內存都會。忽略返回值會把一次細微失敗變成稍後的崩潰或數據損壞;fread/fwrite 即使成功也可能返回短計數,所以務必檢查。
用 ferror/feof 區分錯誤與文件末尾
讀循環結束時,ferror() 和 feof() 告訴你原因:是真正的 I/O 錯誤還是到了文件末尾。EOF 是正常狀態而非錯誤——用 ferror 區分兩者。
12.文件 I/O
stdio 流、逐行與整文件讀取、二進制 I/O、POSIX 描述符、mmap 與目錄遍歷。
打開 / 關閉
fopen 返回 FILE* 流,失敗返回 NULL;fclose 沖刷並釋放它。模式是 r/w/a 加 + 表示讀寫、b 表示二進制。打開失敗後絕不要繼續用那個指針。
讀取整個文件(小文件)
小文件的做法:seek 到結尾、ftell 拿到大小、rewind、malloc size+1、再 fread 讀全。確認讀到的字節數與請求一致——調用之間文件可能變了。
逐行讀取(慣用法)
fgets 安全地讀一行到固定緩衝區,停在換行符或緩衝區上限。用 strcspn 去掉結尾換行——它會被保留在緩衝區裏。這是逐行處理文本的慣用方式。
寫入
fprintf/fputs/fputc 分別向流寫格式化、字符串和字符輸出。錯誤既出現在寫入時也可能在 fclose 時,所以要檢查 fclose 的結果,防止失敗的沖刷被漏掉。
先格式化到字符串(安全)
先用 snprintf 把整段輸出拼進緩衝區,再一次寫出去。這樣能避免併發來源的交錯寫入、寫前可校驗,實際也保持寫入原子性。
二進制 I/O——讀寫原始字節
fread/fwrite 搬運原始字節,最適合定長記錄:恰好讀 sizeof(record) 並處理返回值。短讀不是錯誤就是 EOF——用 ferror/feof 區分。
POSIX 文件描述符
stdio 之下,Linux/macOS 提供對 int 文件描述符的 open/read/write/close:控制精細、無緩衝、坑也更多。要跨界就用 fdopen(fd 換 FILE*)或 fileno(FILE* 換 fd)。
mmap——把文件映射進內存
mmap 把文件直接映射進地址空間,讀寫變成普通內存訪問——大文件很快。munmap 釋放它;文件不能為空,映射大小按頁對齊。
目錄遍歷
opendir/readdir/closedir 逐個列出目錄項。跳過隱含的 '.' 和 '..' 條目,再用 stat 查每個名字的文件類型和大小。
stat——文件元數據
stat/lstat/fstat 返回文件的元數據——大小、修改時間、權限、類型。tmpfile() 給你一個匿名臨時文件,關閉時自動刪除,適合存臨時數據。
13.常見坑
十個經典 C 坑 —— 正確寫法標 GOOD,錯誤寫法標 BAD。
緩衝區溢出
寫過一個固定緩衝區的末尾會破壞相鄰內存,是頂級安全漏洞。給每處寫入加上限:strncpy 手補 NUL、snprintf、或帶長度的 fgets。Sanitizer 能抓到人眼漏掉的問題。
釋放後使用 / 雙重釋放
釋放後使用是未定義行為,往往可被利用;同一塊內存釋放兩次會破壞分配器。free 後立刻把指針置 NULL——free(NULL) 是安全的空操作。
內存泄漏
每塊分配都必須在每條路徑上釋放;泄漏靜默膨脹,直到進程死亡。AddressSanitizer 和 valgrind 默認就會報告——開發期運行,而不是崩潰之後。
= 與 ==
單個 = 是賦值;== 是比較。if (x = 0) 會賦零且永遠為假——一個無聲又難發現的 bug。用 -Wall 編譯,條件裏的賦值要加括號。
有符號溢出是 UB
有符號整數溢出是未定義行為:編譯器可能假定它永遠不會發生並據此優化。加大的有符號值之前先擴成 long long,或者就用你本意的無符號迴繞。
未初始化變量
讀未初始化的局部變量是未定義行為,通常拿到垃圾值。在聲明處初始化每個變量——編譯器 -Wuninitialized 才有真東西可查。
sizeof 中的數組退化
函數內部的數組參數是指針,sizeof 得到 8(指針大小)而非數組大小。sizeof a / sizeof a[0] 只在真正的數組仍在作用域內時有效——顯式傳長度。
差一錯誤
循環邊界是經典的差一錯誤:n 個元素要迭代 i < n(下標 0..n-1),而不是 i <= n 那樣多碰一個越界。大小 n 的數組最後一個合法下標是 n-1。
printf 格式不匹配
格式符與實參類型不匹配是未定義行為——用 %s 配 int 可能直接崩潰。size_t 用 %zu,%d/%ld/%lld 要對上類型,開 -Wformat 抓不匹配。
有符號 / 無符號比較
有符號與無符號比較時,有符號操作數被提升為無符號,負數變成巨大的正數。把兩側都顯式轉成同一有符號類型,或者乾脆別混用。
14.線程與併發
pthread create/join、互斥鎖、條件變量、原子操作與 C11 線程 —— 構建與鏈接加 -pthread。
構建與頭文件
線程程序用 -pthread 編譯,它鏈接 pthread 庫。包含 <pthread.h>,並把每個 pthread_* 的返回值當成要檢查的錯誤——這些 API 不設置 errno。
創建與等待
pthread_create 啓動一個線程運行某個函數,該函數接收一個 void* 參數;pthread_join 阻塞等待它結束。傳入的參數和傳回的 void* 就是跨線程傳遞數據的通道。
通過 void* 傳出數據
線程把結果以 void* 返回——通常是指向 malloc 出來的值的指針。調用者 join 後把返回的指針強轉回來、讀出值、再釋放。
互斥鎖——保護共享狀態
互斥鎖串行化對共享狀態的訪問:訪問前加鎖,之後解鎖。臨界區保持短小——鎖是瓶頸,長時間持有還會引來死鎖。
條件變量——等待 / 通知
條件變量讓線程睡下,直到另一個線程通知某個謂詞改變了。永遠在 while 循環裏等待(有虛假喚醒),並且要持有互斥鎖,醒來後重新檢查謂詞。
分離線程——無需 join
分離線程結束時會自行清理,無需 join。只在確定永遠不會等它的時候才分離——也絕不要把指向調用者棧的指針傳給分離線程,那時棧可能已經沒了。
C11 線程(可移植)
C11 的 <threads.h>(thrd_t)是比 pthreads 更薄、可移植的替代——能力更弱,常常只是對 OS API 的薄封裝。POSIX 上做正經事,pthreads 仍是標準選擇。
原子操作(C11)——<stdatomic.h>
atomic_int 和 atomic_fetch_add 提供有定義內存順序的無鎖加載、存儲和自增——比互斥鎖快的簡單共享計數器,也比普通 int 更正確(不會競爭)。
線程局部存儲(C11)
_Thread_local(C11,GCC 裏是 __thread)讓每個線程擁有自己的變量副本。它適合每線程緩存,也適合不得互相覆蓋的錯誤槽。
線程中的錯誤處理
pthread 函數返回 errno 風格的錯誤碼而不是設置 errno。把返回值接住並用 strerror(rc) 格式化診斷——不檢查的失敗通常表現為神秘的掛起或崩潰。
死鎖規避
死鎖來自“持有所需之物還等待別處所需”。按固定的全局順序加鎖、讓臨界區儘量小、優先一資源一鎖,需要有界等待時用帶超時的鎖。
15.網絡(Socket)
getaddrinfo、TCP 客户端與服務端、最小 HTTP 請求 —— Linux/macOS 下無需額外庫鏈接。
構建與頭文件
POSIX 套接字需要 <sys/socket.h>、<netinet/in.h> 和 <arpa/inet.h>;Linux/macOS 無需額外鏈接庫。用 getaddrinfo 解析地址,不要手填 sockaddr 結構。
TCP 客户端——解析、連接、收發
TCP 客户端用 getaddrinfo 解析服務器、創建套接字、再 connect。地址列表可能很長——逐個嘗試,用第一個成功的,失敗的隨手關掉。
發送 / 接收
send/recv 在已連接的套接字上搬運字節,但都不保證一次搬完。循環直到收滿整條消息或遇到 EOF,一路跟蹤還剩多少字節。
TCP 服務器——socket 流程
服務器創建套接字、設 SO_REUSEADDR、bind 到端口、listen,再循環 accept 連接。每次 accept 返回一個專屬於該客户端的新套接字——監聽套接字從不被讀。
解析主機名 -> IP
getaddrinfo 把主機名和端口變成一列候選地址,順帶處理 IPv4/IPv6 和服務名。逐個試到成功為止,然後用 freeaddrinfo 釋放。
阻塞 vs 非阻塞
套接字默認阻塞:recv 一直等到數據來。O_NONBLOCK 讓調用在沒準備好時返回 EAGAIN,poll/epoll/select 就能在單線程裏監管大量套接字。
UDP——sendto/recvfrom(無連接)
UDP 無連接:sendto/recvfrom 每次都要帶上目標地址,沒有握手、沒有順序、沒有投遞保證。適合對延遲敏感而容忍丟失的流量。
字節序——htons/htonl 與網絡序
網絡字節序是大端;宿主機可能是小端。填 sockaddr 字段、以及從線路上讀數字協議字段時,用 htons/htonl/ntohs/ntohl 轉換。
Windows(Winsock)的區別:
Windows 用 winsock2.h,使用套接字前要先調一次 WSAStartup,用 closesocket() 而不是 close() 關套接字。其餘 API 與 POSIX 套接字一致,代碼大體可移植。
錯誤碼
套接字調用失敗返回 -1 並置 errno——ECONNREFUSED、ETIMEDOUT、ECONNRESET——getaddrinfo 則返回自己的錯誤碼給 gai_strerror。每一步都檢查;這裏的靜默失敗會變成之後的掛起。
HTTPS 需要 TLS 庫
裸套接字只給你 TCP。加密需要 TLS 庫——OpenSSL 的話先建 SSL_CTX,用 SSL_new/SSL_connect 包住套接字,再用 SSL_read/SSL_write——或者用 libcurl 走高層 HTTP+TLS API。
16.時間與日期
牆上時鐘時間、UTC/本地轉換、strftime 格式化與高精度單調計時。
Unix 紀元秒
time(NULL) 返回自 1970-01-01 00:00 UTC 以來的整秒數(time_t)——存儲時間戳用的無時區、普適的瞬間。
轉換為 UTC / 本地 struct tm
gmtime 和 localtime 把 time_t 拆成 struct tm(年、月、日、時……)——分別是 UTC 和本地時區。注意偏移:tm_year 是自 1900 起的年數,tm_mon 是 0-11。
用 strftime 格式化
strftime 用 %Y %m %d %H:%M:%S 等把 struct tm 格式化成可讀文本——日期界的 printf。它是拼日誌行和人類可讀時間戳的標準方式。
把日期解析回 time_t
strptime(POSIX)把文本解析進 struct tm,mktime 再轉成 time_t。把 tm_isdst 設為 -1,讓 mktime 自己處理夏令時而不是猜錯。
已過牆鍾時間
difftime(b, a) 返回兩個 time_t 之間的牆鍾秒數。它簡單,對粗粒度時長夠用,但牆鍾會跳——做測量要用單調時鐘。
高精度單調時鐘
clock_gettime(CLOCK_MONOTONIC) 測量的流逝時間不受 NTP 和夏令時調校影響——基準測試、超時、任何不許跳的計時都用它。
納秒級睡眠(C11 <time.h>)
nanosleep 以 timespec 精度(納秒級)掛起線程。被信號打斷時會報告剩餘時間,好讓調用繼續睡完。
本進程消耗的 CPU 時間
clock() 返回本進程消耗的 CPU 時間,不是牆鍾時間。對比 CPU 與牆鍾時間,能看出任務在等 I/O 而非在計算。
獲取時區偏移
tm_gmtoff(GNU/glibc)保存本地時間相對 UTC 的秒數(東為正),讓你不用猜就能渲染 RFC 822 風格的 +0800 偏移。
不用 strftime 輸出 ISO 8601
用 snprintf 按 UTC struct tm 的字段拼出 ISO 8601 時間戳。輸出與語言環境無關、無歧義——接口和日誌的理想選擇。
17.進程與信號
fork、exec、wait、進程間管道、信號處理與最小守護進程。
平台與頭文件
fork/exec/wait 和信號是 POSIX(Linux/macOS);Windows 用 CreateProcess,模型不同。本節調用需要包含 <unistd.h>、<sys/wait.h> 和 <signal.h>。
fork——克隆當前進程
fork 克隆當前進程。兩份副本從同一返回點繼續:子進程看到 pid 0,父進程看到子進程的 pid。子進程應當 _exit()——從 main 返回會跑父進程的清理並重復沖刷緩衝區。
exec——替換進程映像
exec 用另一個程序替換當前進程映像:pid 不變,代碼全新。它只在失敗時返回(隨後調用 _exit(127),shell 慣例)。環境、文件描述符和工作目錄都會保留。
fork/exec/wait 運行命令
運行外部命令就是 shell 自己的那套:fork(克隆)、execvp(換映像)、waitpid(回收)。WEXITSTATUS 取出子進程的退出碼,WIFSIGNALED 告訴你它是否被信號殺死。
管道——子進程寫、父進程讀
pipe(fd) 創建一對相連的描述符:一端寫、一端讀。fork 之後各進程關掉自己不用的那一端,讀端在寫端關閉時就看到 EOF。
信號處理器
signal() 為異步信號註冊處理器。處理器內部只能調用 async-signal-safe 函數——write、_exit、signal、kill——絕不能 printf 或 malloc,它們不可重入。
sigaction——健壯的 API
sigaction 是 signal() 的健壯替代:能在處理器運行期間屏蔽其他信號、設置 SA_RESTART 恢復被中斷的系統調用、並讓處理器知道是哪個信號觸發的。新代碼優先用它。
臨時屏蔽信號
sigprocmask 在臨界區周圍臨時推遲信號、之後再解除,處理器就不會在共享狀態改了一半時插入。配合一個處理“保存下來的事件”的處理器,信號用法才安全。
殺死進程 / 檢查存在性
kill(pid, sig) 給進程發信號;kill(pid, 0) 只檢查進程存在且你有權發信號,不真的送達。負數 pid 針對整個進程組。
殭屍進程——回收子進程
先於父進程 wait 就退出的子進程會變成殭屍,佔着一個 pid 直到被 wait/waitpid 回收。忽略 SIGCHLD 讓子進程自動被回收,或在處理器裏顯式回收。
守護進程骨架(雙重 fork)
守護進程脱離控制終端:先 fork 讓子進程不是進程組組長,再 setsid 開啓新會話,再 fork 一次,chdir('/'),並把 stdio 重定向到 /dev/null 或日誌。
18.POSIX 正則表達式
regcomp/regexec/regerror:編譯模式、匹配與提取捕獲組、遍歷所有匹配。
設置與頭文件
POSIX 正則(regcomp/regexec)內建於 Linux/macOS 的 libc,無需額外庫。傳 REG_EXTENDED 時使用 ERE 語法——默認的 BRE 是另一套更老的方言。
編譯與匹配
先用 regcomp 編譯一次模式(並檢查返回值!),再用 regexec 對字符串匹配。用 regfree 釋放編譯好的模式——每個 regcomp 都配一個 regfree。
捕獲組
括號分組會落入 regmatch_t 條目:m[1] 保存第 1 組在匹配文本中的起止偏移。用 %.*s 精度技巧配合偏移對打印捕獲片段。
遍歷所有匹配(用 REG_NOTBOL 循環)
要找全部匹配,就在字符串尾部循環 regexec,每次越過一個匹配。第一次迭代之後傳 REG_NOTBOL,避免 ^ 在字符串中途重新錨定。
常用標誌
REG_EXTENDED 開啓 ERE 語法(用它);REG_ICASE 大小寫不敏感匹配;REG_NOSUB 跳過捕獲跟蹤換取提速;REG_NEWLINE 讓 ^/$ 匹配行首行尾而不是整個字符串。
ERE 語法速查
ERE 速查:. 任意字符,^ $ 錨點,[] 字符類,* + ? 重複,{m,n} 有界重複,(a|b) 交替,反向引用,以及括號內的 POSIX 類如 [[:alpha:]]。
替換——沒有內建;手動拼輸出
POSIX 正則沒有內建替換。拼輸出:循環匹配,把匹配之間的文本複製進來,在每個匹配處插入替換串——和遍歷全部匹配的循環是同一個套路。
轉義用户輸入
要把用户輸入嵌進模式裏,先給每個元字符([\\^$.|?*+()[])加上反斜槓轉義。否則精心構造的輸入會改變你模式的含義——這是注入漏洞,不只是邏輯問題。
檢查 regcomp 並 regfree
regcomp 遇到壞模式會失敗——務必檢查返回值並用 regerror 格式化。而且要每個 regcomp 配一個 regfree,否則循環裏反覆匹配會泄漏編譯好的模式。
侷限與性能
複用編譯好的 regex_t:每匹配一次都編譯是浪費。POSIX 正則會回溯,病態模式在不可信輸入上可能很慢——重度或對抗性使用,優先 PCRE2 或 RE2。
編譯期匹配,實現 switch 式分派
啓動時預編譯若干模式,按順序鏈式調用 regexec。當輸入可能匹配多種形狀時,它讀起來像一張清晰的路由表——比一牆字符串比較強。
19.構建與調試
編譯器標誌、最小 Makefile、gdb、消毒器與 valgrind —— 找出 FAQ 章節所警告 bug 的工具。
常用 gcc/clang 標誌
-std=c17 固定語言,-Wall -Wextra 開啓有用警告(一直開),-g 加調試信息,-O0/-O2 選優化級別,-fsanitize=address,undefined 加運行時檢查。零成本還能抓到真 bug。
Makefile——極簡構建驅動
極簡 Makefile 把編譯器、標誌、目標名和對象列表放進變量,依靠隱式規則 %.o:%.c,再提供一個 clean 目標。make 只重建有變化的部分——這正是它的意義。
調試構建 + 發佈構建
調試構建用 -g -O0 保留完整符號、不優化;發佈構建用 -O2 -DNDEBUG,順帶把 assert 編譯掉。兩個目標都留着,才能在用户跑的確切配置下復現崩潰。
gdb 基礎
gdb ./prog,然後:break main、run、next/step 逐行、print x 查看、backtrace 看調用棧、continue、quit。配合 -batch -ex 還能非交互地給 CI 生成崩潰轉儲。
AddressSanitizer(ASan)
編譯時加 -fsanitize=address,undefined,程序就會在運行時帶着棧回溯精確報告溢出、釋放後使用和泄漏。比 valgrind 快也清晰——第一個要上的工具。
valgrind 內存檢測
valgrind --leak-check=full ./prog 無需重編譯就能報告非法讀寫、釋放後使用和確定泄漏——但慢約 20-50 倍。在無法重編譯或需要更多細節時使用。
UBSan 標誌
-fsanitize=undefined 在運行時用精確報告抓有符號溢出、未對齊訪問等未定義行為。和 -fsanitize=address 組合,一次構建同時覆蓋內存與 UB。
編譯到彙編 / 預處理
gcc -S 輸出彙編,gcc -E 顯示預處理後的源碼,gcc -c 生成不鏈接的目標文件。每個都是一扇看編譯器與預處理器到底做了什麼的小窗。
靜態分析(可選)
gcc -fanalyzer、clang --analyze 和 cppcheck 不用運行程序就能找 bug——空指針解引用、泄漏、未初始化使用。加進 CI 很便宜,還能抓到測試漏掉的錯誤類別。
構建系統
Make 適合中小項目;CMake 生成 Make 或 Ninja 文件,跨平台可移植;Meson 更新也更快。單個文件的話,一個 Makefile 或 shell 別名就夠。
關於本速查
本頁是 ISO C(C11/C17)的自包含速查手冊,覆蓋語言本身和標準庫在系統編程中約 80% 的常見用法,外加真實項目中常用的併發、Socket、正則與構建工具。權威參考見 C11 標準(ISO/IEC 9899:2011)與 cppreference 的 C 章節。 19 個章節各自聚焦一個主題 —— 從你的第一個程序、指針,到字符串、內存所有權與常見坑。每章拆分為 6–10 個子主題,附 5–15 行簡潔示例。代碼塊刻意短小自明,每塊都有複製按鈕,可直接粘貼到編譯器。 全部內容在你的瀏覽器內運行 —— 不上傳、不追蹤。本頁是 GuruToolkit 免費開發者工具集的一部分;這裏的代碼片段可自由使用,不提供任何擔保。
版本 2.3.0