跳轉到

1999:C99 重大修訂——停滯十年的大躍進#

事件#

1999 年,ISO 發佈 ISO/IEC 9899:1999(C99)。距離 C89 已十年,這段期間 C++、Java 皆以「C 的下一代」姿態崛起,C 需要一次大修訂證明自己沒有落伍。C99 是 C 語言史上最大的語法擴充。

為何重要#

  • 新語法一覽:行內註解 //、long long(64 位元)、可變長度陣列(VLA)、彈性陣列成員、指定初始化(designated initializers)、複合字面值(compound literals)、inline 函式、stdint.h(int32_t 等)、宣告可與程式碼混排、for 內宣告變數、複數型別 _Complex、變數參數巨集 __VA_ARGS__、snprintf。

理論與實用原因(各新語法為何加入)#

  • // 行內註解:
  • 彌補的缺陷:C90 只有 /* */,無法巢狀,一行小註解要寫四個符號。
  • 實用原因:C++ 已證明 // 深受歡迎,C99 直接吸收,降低 C/C++ 混用專案的摩擦。
  • long long(至少 64 位元):
  • 彌補的缺陷:C90 的 long 在多數平台只有 32 位元,1990 年代檔案系統與記憶體已突破 4GB,int 溢位成為真實的資安與正確性問題。
  • 實用原因:64 位元運算普及,需要標準化的 64 位元整數。
  • stdint.h(int32_t、uint64_t、intptr_t):
  • 彌補的缺陷:C 的 int/long 長度依平台而定,同一份程式碼在不同機器上語意不同,是無數移植 bug 的根源。
  • 理論原因:明確寬度的整數讓「演算法的正確性」與「平台實作細節」解耦。
  • 實用原因:網路協定、檔案格式、密碼學都需要精確寬度。
  • 指定初始化 { [3] = 1, .x = 2 }:
  • 彌補的缺陷:C90 只能照順序初始化,struct 加一個欄位,所有初始化都要重排。
  • 實用原因:OS 核心大量使用大型 struct 與稀疏陣列(如 file_operations 的函式表),指定初始化讓程式碼與欄位順序解耦——Linux 核心因此全面採用。
  • 宣告混排與 for 內宣告:
  • 彌補的缺陷:C90 強制所有宣告在區塊開頭,變數離使用處太遠、意義不明。
  • 理論原因:呼應「最小作用域」原則,變數宣告越靠近使用處,程式越易讀且越不易誤用。
  • VLA(可變長度陣列):
  • 彌補的缺陷:C90 陣列長度必須是編譯期常數,執行期決定大小只能 malloc/free。
  • 理論原因:讓陣列也有 Alloca 式的執行期分配,語法更自然。
  • 爭議:VLA 有堆疊溢位風險,C11 將其降為選用——這是標準化「過頭再回收」的著名案例。
  • inline 函式:
  • 彌補的缺陷:C90 的巨集(#define)無型別檢查、有副作用陷阱。
  • 實用原因:inline 函式兼顧巨集的零呼叫開銷與函式的型別安全,吸收 C++ 的經驗。
  • 變數參數巨集 __VA_ARGS__:
  • 彌補的缺陷:無法用巨集包裝 printf 風格的除錯函式(如 LOG("x=%d", x)),各專案各自為政。

彌補了什麼缺陷#

C99 彌補了 C90「十年未更新、64 位元缺標準、初始化死板、作用域限制、巨集不安全、無國際化函式庫」的缺陷,同時吸收 C++ 的成功經驗(//、inline、宣告混排),確保 C 在系統程式設計的統治地位延續。

程式範例#

// 行內註解——一行小註解不再需要四個符號:

/* C90:一行小註解也要四個符號,且無法巢狀 */
int x = 1;  /* 計數器 */

/* C99: */
int x = 1;  // 計數器
// int y = /* 巢狀 */ 2;   // 整行停用,含巢狀註解也不會出錯

long long 與 stdint.h——64 位元時代的必備品:

/* C90 的缺陷:long 只有 32 位元,檔案超過 4GB 就溢位 */
long bigfile = 5000000000;          /* 溢位!結果錯誤 */

/* C99: */
long long bigfile = 5000000000LL;   /* 至少 64 位元 */

#include <stdint.h>
/* C 的缺陷:int/long 長度依平台而定,移植 bug 叢生 */
/* C99:明確寬度,演算法正確性與平台解耦 */
int32_t  packet_len;                /* 網路協定需要精確 32 位元 */
uint64_t disk_bytes;                /* 磁碟容量用無號 64 位元 */
intptr_t addr_as_int;               /* 可放指標的整數 */

指定初始化——Linux 核心的函式表因此全面採用:

/* C90 的缺陷:只能照順序初始化,struct 加欄位,全部要重排 */
struct point { int x; int y; int z; };
struct point p = {1, 2, 0};         /* z 是哪個?欄位順序一改就出錯 */

/* C99:指定初始化,與順序解耦 */
struct point p = { .x = 1, .y = 2 };

/* Linux 核心的真實應用:稀疏函式表 */
struct file_operations fops = {
    .owner   = THIS_MODULE,
    .read    = my_read,
    .write   = my_write,
    /* 未指定的成員自動為 NULL——不用數第幾個欄位 */
};

int sparse[100] = { [3] = 1, [77] = 2 };   /* 稀疏陣列:只有兩格非零 */

宣告混排與 for 內宣告——變數靠近使用處:

/* C90 的缺陷:所有宣告擠在區塊開頭,離使用處太遠 */
void old(void) {
    int i; char *p; long total;    /* 這些是做什麼的?要往回翻 */
    for (i = 0; i < 10; i++) { ... }
}

/* C99:宣告與程式碼混排,最小作用域 */
void modern(void) {
    for (int i = 0; i < 10; i++) { ... }   /* i 只活在迴圈內,不會被誤用 */
    long total = 0;                        /* 用之前才宣告 */
}

VLA——執行期決定大小的陣列(注意堆疊溢位風險):

/* C90:陣列長度必須是編譯期常數 */
int buf[512];
/* 執行期大小只能 malloc/free */

/* C99:VLA */
void process(int n) {
    int buf[n];          /* 執行期決定大小,自動在函式結束時釋放 */
    for (int i = 0; i < n; i++) buf[i] = i;
}
/* 危險:n = 100000000 時堆疊溢位——這是 C11 將 VLA 降為選用的原因 */

inline 函式——巨集的零開銷 + 函式的型別安全:

/* C90 巨集的缺陷:無型別檢查、副作用陷阱 */
#define SQR(x) ((x) * (x))
int i = 3;
int a = SQR(i++);        /* 展開成 ((i++)*(i++))——i 被加兩次! */

/* C99:inline 兼顧兩者 */
static inline int sqr(int x) { return x * x; }
int a = sqr(i++);        /* i 只加一次,型別有檢查,零呼叫開銷 */

變數參數巨集 __VA_ARGS__——除錯函式終於能標準化:

#include <stdio.h>

/* C90:無法用巨集包裝 printf 風格函式,各專案各自為政 */
/* C99: */
#define LOG(fmt, ...) \
    fprintf(stderr, "[LOG %s:%d] " fmt "\n", __FILE__, __LINE__, __VA_ARGS__)

LOG("x=%d y=%d", 1, 2);  /* 輸出:[LOG test.c:7] x=1 y=2 */

snprintf——緩衝區溢位剋星:

/* C90 的 sprintf:不知道緩衝區多大,溢位 CVE 常客 */
char buf[16];
sprintf(buf, "%s-%d", longname, num);   /* 超過 16 bytes?直接炸 */

/* C99 的 snprintf:明確上限 */
snprintf(buf, sizeof buf, "%s-%d", longname, num);   /* 安全截斷 */

相關條目#