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); /* 安全截斷 */