代碼審查時發現一段異常的 `switch` 結構:多個 `case` 標簽堆疊、中間缺失 `break`。這觸發了 C 語言 `switch` 的核心機制——**無條件穿透**。本文通過標準規定、汇编視角、實測驗證,徹底講透這一「特性而非 Bug」的行為。
switch (error) {
case ENUM_0:
case ENUM_1:
printf("case 0 and 1\n");
case ENUM_2: // ⚠️ 此處無 break!
printf("case 2\n");
break;
}
核心疑問: 當 `error == ENUM_0` 或 `ENUM_1` 時,`printf("case 0 and 1")` 執行後,會不會繼續執行 `case ENUM_2` 的代碼?
switch 語句將控制權轉移到匹配的 case 標簽,隨後順序執行後續所有語句,直到遇到 break、return、goto 或 switch 結束。
#include <stdio.h>
int main(void) {
char ch = 0; // 測試 ENUM_0 情況
switch (ch) {
case 0:
case 1:
printf("case 0 excute\n");
case 2:
printf("case 2 excute\n");
break;
}
return 0;
}
case 0 excute
case 2 excute
鐵證: `ch=0` 匹配 `case 0:`,執行第一個 `printf`,**隨後穿透執行 `case 2:` 的 `printf`**,最後遇到 `break` 跳出。`case 1:` 同理。
main:
mov BYTE PTR [rbp-1], 0 ; ch = 0
mov eax, BYTE PTR [rbp-1] ; 載入 ch
cmp al, 2
ja .L_default ; 若 >2 跳默認/結束
jmp QWORD PTR [rdx+rax*8] ; 跳轉表:索引 rax 跳轉
.L_case_0: ; case 0: 入口
lea rdi, [rip+.LC0] ; "case 0 excute\n"
call puts
.L_case_1: ; case 1: 入口 (無代碼,直接穿透)
.L_case_2: ; case 2: 入口
lea rdi, [rip+.LC1] ; "case 2 excute\n"
call puts
jmp .L_break ; break → 跳出
.L_break:
xor eax, eax
ret
.L_default:
xor eax, eax
ret
關鍵觀察:
| 誤區/風險 | 正確認知/對策 |
|---|---|
| 以為 case 之間有隱式隔離 | 無隔離,必須顯式 break/return 終止 |
| 忘寫 break 導致邏輯錯誤 | 編譯器 `-Wimplicit-fallthrough` (GCC/Clang) 可檢測並警告 |
| 故意利用穿透但無註釋 | **必須加註釋** `/* fallthrough */` 或 C++17 `[[fallthrough]];` 標記意圖 |
| default 位置影響穿透 | default 只是特殊標簽,位置任意,穿透規則同樣適用 |