學習目標
- 理解「敘述(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 階梯。
練習題
- 基本題:給定
int n = -7;,用 if/else-if/else 判斷並印出positive、negative或zero。 - BMI 分級:以寫死的身高體重計算 BMI,用 else-if 階梯印出過輕(<18.5)、正常(<24)、過重(<27)、肥胖(≥27)。注意條件順序。
- switch 月份天數:給定月份 1~12,用 switch 印出該月天數(2 月印 28/29),用 fall-through 讓同天數的月份共用 case。
- 字元分類:給定一個
char c,用 else-if 階梯判斷是大寫字母、小寫字母、數字、空白或其他(先不要用<ctype.h>,自己比較範圍)。 - 懸掛 else 排雷:把觀念講解中的懸掛 else 範例親手打一遍,先預測輸出、再執行驗證,最後用大括號改成兩種不同語意的版本。
- 挑戰題(簡易計算機):給定兩個
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