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)。

常見錯誤與陷阱

  1. VLA 當萬靈丹:int buf[user_input] 是 stack overflow 的邀請函; 大小不可控時用 malloc,VLA 只適合小而有界的情況。
  2. restrict 亂承諾:對實際上有別名的指標加 restrict,最佳化後行為錯亂 且極難除錯——不確定就不要加。
  3. 舊編譯器/舊專案的相容性:把 C23 的 auto、constexpr 寫進要支援 舊工具鏈的程式碼會直接編譯失敗;公共程式庫要明確宣告最低標準版本。
  4. _Generic 的型別匹配是「不含轉換」的:_Generic(3.5f, double: ...) 不會匹配 float,每個要支援的型別都要各列一項。
  5. 數字分隔符寫成底線:C23 用 1'000'000(單引號), 寫 1_000_000(其他語言的習慣)是語法錯誤。

練習題

  1. 【易】把一段 C89 風格程式(變數全在開頭宣告、/* */ 註解、0/1 當布林) 重構成 C99 風格。
  2. 【易】用指定初始化器建一個 int days[13],讓 days[1]~days[12] 是各月份天數(索引 0 不用)——對照 K&R §5.7 的寫法。
  3. 【中】用彈性陣列成員實作 struct string { size_t len; char data[]; } 與其建構函式 string_new(const char *),比較「兩次 malloc」與「一次 malloc」。
  4. 【中】用 _Generic 寫一個 print_any(x) 巨集,支援 int/double/char/void 四種型別自動選對 printf 格式。
  5. 【難】用 C23 constexpr 與 enum 底層型別重寫位元旗標單元的權限系統, 並用 static_assert 驗證結構大小。
  6. 【難】寫一個「特性偵測」程式:用 __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 時代的實務指南