K&R 第二版(1988)描述的是 ANSI C(C89/C90)。此後 C 語言經歷了四次重要修訂: C99、C11、C17、C23。好消息是:K&R 教你的一切依然成立;新標準是「增補」而非推翻。 本單元把 K&R 之後的演進一次補齊,讓你讀得懂(也寫得出)現代 C 程式碼。
學習目標
- 掌握 C99 的日常必備:
//註解、宣告位置自由、stdbool.h、stdint.h、 複合字面值、指定初始化器、snprintf - 理解 C99 進階特性:VLA、彈性陣列成員、
inline、restrict、變參巨集 - 掌握 C11 重點:
_Static_assert、匿名結構/union、_Generic、對齊控制、threads.h與stdatomic.h概觀 - 知道 C17 的定位(缺陷修正版,無新特性)
- 認識 C23 的重大變化:
bool/true/false成為關鍵字、nullptr、auto、constexpr、typeof、二進位字面值、數字分隔符、[[attributes]]、#embed、_BitInt - 會用
-std=旗標選擇語言標準並測試特性支援度
對照章節 / 參考來源
- K&R 附錄 C(C89 相對 K&R C 的變化——歷史對照)
- ISO/IEC 9899:1999(C99)、9899:2011(C11)、9899:2018(C17)、9899:2024(C23)
- cppreference:C language history
- 《Modern C》(Jens Gustedt),free PDF;C23 版已更新
觀念講解
1. C99 —— 最重要的一次升級
日常必備(今天所有 C 程式碼的基本盤):
// 單行註解(借自 C++)
for (int i = 0; i < n; i++) /* 宣告寫進 for,位置自由 */
#include <stdbool.h>
bool ok = true; /* 有名有姓的布林 */
#include <stdint.h>
uint32_t crc; /* 固定寬度整數:跨平台的救星 */
int64_t big;
uintptr_t addr; /* 能裝下指標的整數 */
long long x = 9000000000LL; /* 至少 64 位 */
snprintf(buf, sizeof buf, "%d", v); /* 永遠用它取代 sprintf */
複合字面值(compound literals)——匿名的臨時物件:
draw_point((struct point){ .x = 3, .y = 4 });
int *tmp = (int[]){ 1, 2, 3 };
指定初始化器(designated initializers)——依名字/索引初始化:
struct config c = { .port = 8080, .verbose = true }; /* 其餘為 0 */
int lut[256] = { ['a'] = 1, ['e'] = 1, ['i'] = 1 };
變長陣列 VLA——大小是執行期值的自動陣列:
void f(int n) { double m[n][n]; /* ... */ }
注意:VLA 配置在堆疊上,n 太大直接 stack overflow;C11 把它降為選配,
Linux 核心明令禁用。教材立場:知道它存在,實務優先 malloc。
彈性陣列成員(flexible array member)——「header + 變長資料」的正規寫法:
struct packet {
int len;
char data[]; /* 必須是最後一個成員 */
};
struct packet *p = malloc(sizeof *p + n); /* 一次配置、一次釋放 */
inline 與 restrict:inline 建議編譯器內聯(配合 header 使用有連結細節);
restrict 是程式員對編譯器的承諾——「這個指標是存取該記憶體的唯一途徑」,
讓 memcpy 等函式能積極最佳化(違反承諾是 UB)。
變參巨集:
#define LOG(fmt, ...) fprintf(stderr, "[log] " fmt "\n", ##__VA_ARGS__)
2. C11 —— 安全與並行
/* 編譯期檢查:條件不成立直接編譯錯誤 */
_Static_assert(sizeof(void *) == 8, "need 64-bit");
/* 匿名結構/union:成員直接「浮上來」 */
struct value {
int type;
union { /* 不用取名字 */
int i;
double d;
}; /* v.i、v.d 直接用 */
};
/* _Generic:依型別在編譯期選擇運算式(type-generic 巨集) */
#define type_name(x) _Generic((x), int: "int", double: "double", \
char *: "char *", default: "other")
/* 對齊控制 */
_Alignas(64) char cacheline[64];
size_t a = _Alignof(max_align_t);
並行:C11 新增 threads.h(thrd_create 等)與 stdatomic.h(原子型別與操作)、
定義了跨執行緒的記憶體模型。現實:macOS 至今未提供 threads.h(glibc 2.28+ 才有),
跨平台程式仍以 pthreads 為主;stdatomic.h 則 clang/gcc 都支援。
_Noreturn(函式不返回)也是 C11 加入的(C23 改為 [[noreturn]])。
3. C17 —— 修正版
C17(正式名 C18)沒有任何新特性,只修正 C11 的缺陷報告(DR),
是「更穩的 C11」。__STDC_VERSION__ 為 201710L。
目前多數編譯器的預設 -std 就是 gnu17。
4. C23 —— 三十年來最大步
| 特性 | 說明 |
|---|---|
bool/true/false 關鍵字 |
不再需要 <stdbool.h> |
nullptr |
型別為 nullptr_t 的空指標常量,取代模糊的 NULL |
auto 型別推斷 |
auto x = 3.5;(僅限有初始化器的物件宣告) |
constexpr |
真正的編譯期常數(可當陣列大小、case 標籤) |
typeof / typeof_unqual |
取運算式的型別(GNU 擴充轉正) |
二進位字面值 0b1010 |
位元旗標終於能直觀地寫 |
數字分隔符 1'000'000 |
可讀性(注意是單引號) |
[[attributes]] |
[[nodiscard]]、[[maybe_unused]]、[[fallthrough]]、[[deprecated]]、[[noreturn]] |
#embed |
前置處理期把二進位檔案內容嵌入陣列 |
_BitInt(N) |
精確位元寬度整數(如 _BitInt(12)),字尾 wb |
| enum 底層型別 | enum e : unsigned char { ... } |
static_assert/alignas/alignof/thread_local |
轉為正式關鍵字(不再需要底線版) |
空括號 f() 即 f(void) |
舊式「不定參數」宣告正式移除 |
| K&R 舊式函式定義移除 | f(a, b) int a, b; {} 正式出局 |
其他:__VA_OPT__、unreachable()、memset_explicit、strdup 入標準、
十進位浮點(選配)、printf 的 %b(二進位輸出)。
Apple clang 21 實測結果(本教材在 macOS 驗證):上表所有特性
——含 constexpr、auto、typeof、nullptr、_BitInt、#embed、
enum 底層型別——皆可用 -std=c2x 編譯執行。範例 03_c23_features.c
只包含實測通過的特性。
5. 版本速查
| 標準 | __STDC_VERSION__ |
編譯旗標 | 一句話 |
|---|---|---|---|
| C89/C90 | (未定義) | -std=c89 |
K&R 第二版描述的 ANSI C |
| C99 | 199901L |
-std=c99 |
日常特性大放送 |
| C11 | 201112L |
-std=c11 |
安全+並行+泛型選擇 |
| C17 | 201710L |
-std=c17 |
C11 修正版 |
| C23 | 202311L |
-std=c2x / -std=c23 |
現代化大改版 |
範例程式
| 檔案 | 內容 | 編譯 / 執行 |
|---|---|---|
01_c99_features.c |
C99 全特性實演:stdbool/stdint/複合字面值/指定初始化/VLA/彈性陣列成員/變參巨集/snprintf | cc -std=c11 -Wall -Wextra -o out 01_c99_features.c && ./out |
02_c11_features.c |
C11 實演:_Static_assert/匿名結構union/_Generic/alignas/alignof/stdatomic 概觀 | cc -std=c11 -Wall -Wextra -o out 02_c11_features.c && ./out |
03_c23_features.c |
需 C23:關鍵字 bool/nullptr/auto/constexpr/typeof/0b/分隔符/attributes/#embed/_BitInt/enum 底層型別 | cc -std=c2x -Wall -Wextra -o out 03_c23_features.c && ./out |
注意:前兩檔用 -std=c11 編譯;03_c23_features.c 必須用 -std=c2x
(Apple clang)或 -std=c23(新版 gcc/clang)。
常見錯誤與陷阱
- VLA 當萬靈丹:
int buf[user_input]是 stack overflow 的邀請函; 大小不可控時用 malloc,VLA 只適合小而有界的情況。 restrict亂承諾:對實際上有別名的指標加 restrict,最佳化後行為錯亂 且極難除錯——不確定就不要加。- 舊編譯器/舊專案的相容性:把 C23 的
auto、constexpr寫進要支援 舊工具鏈的程式碼會直接編譯失敗;公共程式庫要明確宣告最低標準版本。 _Generic的型別匹配是「不含轉換」的:_Generic(3.5f, double: ...)不會匹配float,每個要支援的型別都要各列一項。- 數字分隔符寫成底線:C23 用
1'000'000(單引號), 寫1_000_000(其他語言的習慣)是語法錯誤。
練習題
- 【易】把一段 C89 風格程式(變數全在開頭宣告、
/* */註解、0/1 當布林) 重構成 C99 風格。 - 【易】用指定初始化器建一個
int days[13],讓days[1]~days[12]是各月份天數(索引 0 不用)——對照 K&R §5.7 的寫法。 - 【中】用彈性陣列成員實作
struct string { size_t len; char data[]; }與其建構函式string_new(const char *),比較「兩次 malloc」與「一次 malloc」。 - 【中】用
_Generic寫一個print_any(x)巨集,支援 int/double/char/void 四種型別自動選對 printf 格式。 - 【難】用 C23
constexpr與 enum 底層型別重寫位元旗標單元的權限系統, 並用static_assert驗證結構大小。 - 【難】寫一個「特性偵測」程式:用
__STDC_VERSION__與__has_include/__has_c_attribute條件編譯,在 C99/C11/C23 下都能編譯 並回報當前環境支援哪些特性。
延伸閱讀
- Jens Gustedt,《Modern C》(涵蓋到 C23,作者是 C 標準委員會成員)
- cppreference 的 C23 專頁與各特性條目
- JeanHeyd Meneide(C 委員會)部落格:#embed 與 C23 的幕後故事
- clang/gcc 文件的 C standards support 頁(特性支援矩陣)
- 《Effective C, 2nd Edition》(Robert Seacord):C17/C23 時代的實務指南