學習目標

  • 理解「敘述(statement)」與「區塊(block / compound statement)」的概念。
  • 熟練使用 if、if/else、else-if 階梯撰寫多路分支。
  • 認識「懸掛 else(dangling else)」問題,理解 else 永遠配對「最近的未配對 if」。
  • 掌握 switch 的語法、break 的必要性、fall-through 的正確與錯誤用法。
  • 能依情境判斷該用 else-if 階梯還是 switch。

對照 K&R 章節

  • K&R §3.1 Statements and Blocks
  • K&R §3.2 If-Else
  • K&R §3.3 Else-If
  • K&R §3.4 Switch

觀念講解

1. 敘述與區塊

  • 運算式敘述:任何運算式加上分號就是一個敘述,如 x = 5;、printf("hi");、甚至 ;(空敘述)。
  • 區塊(複合敘述):用 { } 把多個敘述包成一個單位。語法上,區塊出現的位置等同單一敘述。
if (x > 0)
    printf("positive\n");        /* 單一敘述可以不加大括號 */

if (x > 0) {                     /* 但加大括號永遠不會錯 */
    printf("positive\n");
    count++;
}

建議:一律加大括號。 歷史上多次重大安全漏洞(如 Apple 的 goto fail)都與省略大括號有關。

2. if / else

if (score >= 60) {
    printf("pass\n");
} else {
    printf("fail\n");
}
  • 條件是任何「純量運算式」:非零為真、零為假。所以 if (n) 等同 if (n != 0)。
  • else 是選配的。

3. else-if 階梯

多路互斥判斷的標準寫法(K&R §3.3):

if (score >= 90) {
    grade = 'A';
} else if (score >= 80) {
    grade = 'B';
} else if (score >= 70) {
    grade = 'C';
} else if (score >= 60) {
    grade = 'D';
} else {
    grade = 'F';
}
  • 條件由上而下依序測試,第一個成立的分支執行後整個階梯結束。
  • 條件的「順序」很重要:上例若把 >= 60 放最前面,90 分也會拿到 D。
  • 最後的 else 是「以上皆非」的收尾,也常用來攔截「不可能的值」以便除錯。

4. 懸掛 else(dangling else)

if (a > 0)
    if (b > 0)
        printf("both positive\n");
else                                 /* 縮排騙了你! */
    printf("a is not positive\n");   /* 實際上配對的是「內層」if */

規則:else 配對最近的、還沒有 else 的 if,與縮排無關。上面的 else 屬於 if (b > 0)。

修正:用大括號明確表達意圖:

if (a > 0) {
    if (b > 0) {
        printf("both positive\n");
    }
} else {
    printf("a is not positive\n");
}

5. switch

switch 依「整數運算式」的值跳到對應的 case 標籤:

switch (grade) {
case 'A':
case 'B':                        /* A、B 共用同一段:刻意的 fall-through */
    printf("good\n");
    break;
case 'C':
    printf("ok\n");
    break;
default:                         /* 都不符合時執行 */
    printf("needs work\n");
    break;
}

重點整理:

規則 說明
case 標籤必須是整數常數運算式 不能是變數、範圍或字串(字元常數可以,因為是整數)
執行完不會自動跳出 沒有 break 就會掉落(fall through)到下一個 case
default 位置不限 慣例放最後;建議永遠寫上
同值 case 不可重複 編譯錯誤

fall-through 是雙面刃:多個 case 共用同一段程式(如上例 A、B)是正當用法; 但「忘記寫 break」是史上最常見的 C bug 之一。刻意 fall-through 時務必加註解 /* fall through */。

6. else-if vs switch 怎麼選?

情境 建議
判斷「範圍」(score >= 90) else-if 階梯(switch 做不到範圍)
判斷「離散值」(選單選項、字元指令) switch(意圖清楚,編譯器可查缺漏)
條件涉及多個變數 else-if 階梯
分支非常多的離散值 switch(部分編譯器可最佳化成跳躍表)

範例程式

檔案 內容 編譯與執行
01_if_else_ladder.c if/else 基礎、else-if 成績分級、條件順序的重要性 cc -std=c11 -Wall -Wextra -o 01_if_else_ladder 01_if_else_ladder.c && ./01_if_else_ladder
02_dangling_else.c 懸掛 else 實測:縮排誤導 vs 大括號正解 cc -std=c11 -Wall -Wextra -o 02_dangling_else 02_dangling_else.c && ./02_dangling_else
03_switch_fallthrough.c switch 完整示範:正常 break、刻意 fall-through、default、簡易計算機 cc -std=c11 -Wall -Wextra -o 03_switch_fallthrough 03_switch_fallthrough.c && ./03_switch_fallthrough

常見錯誤與陷阱

陷阱 1:if 條件後面多打一個分號

if (x > 0);                  /* 這個分號是「空敘述」,if 到此結束! */
{
    printf("always runs\n"); /* 這個區塊無條件執行 */
}

修正:刪掉分號。-Wall(clang 的 -Wempty-body)通常會警告。

陷阱 2:switch 忘記 break

switch (option) {
case 1:
    printf("one\n");         /* 少了 break */
case 2:
    printf("two\n");         /* option == 1 時這行也會執行! */
    break;
}

修正:每個 case 結尾加 break;;刻意掉落時寫註解 /* fall through */。

陷阱 3:在 case 內宣告變數卻沒加大括號

switch (n) {
case 1:
    int x = 10;              /* 錯誤:標籤後不能直接接宣告(C17 前) */
    printf("%d\n", x);
    break;
}

修正:把該 case 的內容包進大括號 case 1: { int x = 10; ...; break; }。

陷阱 4:用 == 串接多個比較對象

if (c == 'y' || 'Y') { ... }     /* 恆真:'Y' 本身非零 */

正確寫法是每個比較都完整:if (c == 'y' || c == 'Y')。

陷阱 5:浮點數當 switch 條件

switch (price) { ... }           /* 編譯錯誤:switch 只接受整數型別 */

範圍或浮點判斷請用 else-if 階梯。

練習題

  1. 基本題:給定 int n = -7;,用 if/else-if/else 判斷並印出 positive、negative 或 zero。
  2. BMI 分級:以寫死的身高體重計算 BMI,用 else-if 階梯印出過輕(<18.5)、正常(<24)、過重(<27)、肥胖(≥27)。注意條件順序。
  3. switch 月份天數:給定月份 1~12,用 switch 印出該月天數(2 月印 28/29),用 fall-through 讓同天數的月份共用 case。
  4. 字元分類:給定一個 char c,用 else-if 階梯判斷是大寫字母、小寫字母、數字、空白或其他(先不要用 <ctype.h>,自己比較範圍)。
  5. 懸掛 else 排雷:把觀念講解中的懸掛 else 範例親手打一遍,先預測輸出、再執行驗證,最後用大括號改成兩種不同語意的版本。
  6. 挑戰題(簡易計算機):給定兩個 double 與一個運算子字元(+ - * /),用 switch 計算並印出結果;除以零時印出錯誤訊息而不是讓程式產生 inf/nan。

延伸閱讀

  • K&R 2/e §3.1–3.4;習題 3-1、3-2
  • Apple「goto fail」漏洞分析(CVE-2014-1266)——省略大括號的真實代價
  • C11 標準 §6.8.4(selection statements)
  • C23 的 [[fallthrough]] 屬性——現代編譯器如何幫你標記刻意掉落
  • 下一單元預告:迴圈——while、for、do-while