C 語言看似簡單,實則暗藏許多「未定義行為」「實作定義行為」與「反直覺規則」。本文透過六個經典問題,從標準規範、編譯器實作、組合語言層面深度解析。
while(1) {
fprintf(stdout, "stdout.");
fprintf(stderr, "stderr.");
sleep(1);
}
// 實際輸出可能 stderr 先於 stdout 出現
stdout 預設行緩衝 (終端) 或全緩衝 (管道/檔案)stderr 預設無緩衝
setbuf(stdout, NULL); // 關閉 stdout 緩衝
// 或
fflush(stdout); // 手動刷新
// 或
fprintf(stdout, "stdout.\n"); // 換行觸發行緩衝刷新
#define PRINT_D(x) printf(#x " is %d\n", (x))
char *str = "我"; // UTF-8 編碼,3 bytes
PRINT_D(printf(str));
// 輸出: printf(str) is 3 (非 1)
printf 回傳實際輸出位元組數,非字元數
int a = 1;
switch(a) {
int b = 2; // 永不執行!
case 1: PRINT_D(b); break;
default: PRINT_D(b); break;
}
// 輸出亂碼 (b 未初始化)
C 標準:switch 語句體中,首個 case/default 標籤前的語句永遠不會被執行。
// case 標籤編譯為直接跳轉
jne L_case1 // 跳過初始化代碼
jmp L_default
L_case1:
// 直接執行 case 內容
switch(a) {
case 1: {
int b = 2; // 放入區塊內
PRINT_D(b);
break;
}
// 或
int b = 2; // 移至 switch 外
switch(a) { ... }
}
int i = 1;
PRINT_D(sizeof(i++)); // 輸出 4 (int 大小)
PRINT_D(i); // 輸出 1 (未遞增!)
sizeof 是編譯期運算子 (VLA 除外)mov $4, %ebx 推入堆疊
int n = 10;
sizeof(int[n++]); // 這裡 n 會遞增!(C99 VLA 特例)
char ch = 2["hello"]; // 等同 "hello"[2] → 'l'
a[b] 定義為 *(a + b)
2["hello"] → *(2 + "hello") → *("hello" + 2) → "hello"[2]
陣列存取完全等價於指標算術,編譯器無「陣列下標」概念,只有「指標偏移」。
int d = 2;
PRINT_F(d); // %f 對應 float/double
// 輸出亂碼 (如 0.000000)
printf 為可變參數函數,無型別檢查%f 期望 double (8 bytes),實際傳 int (4 bytes)
typedef union {
float f;
struct {
unsigned int mantissa : 23;
unsigned int exponent : 8;
unsigned int sign : 1;
} bits;
} float_ieee754;
// 手動構造 -2.0
float_ieee754 v = {.bits = {.sign=1, .exponent=128, .mantissa=0}};
printf("%f\n", v.f); // -2.000000
| 誤區 | 正確認知 |
|---|---|
| 輸出即時顯示 | 受緩衝模式控制,需手動刷新或換行 |
| printf 回傳字元數 | 回傳位元組數,多位元組字元需注意 |
| switch 可聲明變數 | 標籤前代碼死區,需用區塊或前置聲明 |
| sizeof 執行運算元 | 純編譯期型別運算 (VLA 除外) |
| a[b] 與 b[a] 不同 | 完全等價,本質都是 *(a+b) |
| 格式字串會檢查型別 | 無檢查,必須程式員自行保證匹配 |