學習目標
- 熟練三種迴圈:
while、for、do-while,理解各自的檢查時機與適用場景。 - 會寫巢狀迴圈,理解內外層變數的角色(如九九乘法表的列與欄)。
- 正確使用
break(跳出整個迴圈)與continue(跳過本圈剩餘部分)。 - 認識
goto,理解為什麼現代 C 幾乎只在「多層跳出/集中清理」時使用它。 - 完成三個經典練習:攝氏華氏對照表(K&R 開篇範例)、九九乘法表、質數判斷。
對照 K&R 章節
- K&R §1.3 The For Statement(溫度對照表)
- K&R §3.5 Loops—While and For
- K&R §3.6 Loops—Do-While
- K&R §3.7 Break and Continue
- K&R §3.8 Goto and Labels
觀念講解
1. while:先檢查、後執行
int i = 0;
while (i < 5) { /* 先檢查條件,成立才進入本體 */
printf("%d ", i);
i++; /* 別忘了推進!否則無窮迴圈 */
}
條件一開始就不成立的話,本體一次都不會執行。 while 適合「事先不知道要跑幾次」的情況(例如:讀到 EOF 為止)。
2. for:計數迴圈的標準寫法
for (int i = 0; i < 5; i++) {
printf("%d ", i);
}
for (初始化; 條件; 推進) 三個部分一目瞭然,等價於:
初始化;
while (條件) {
本體;
推進;
}
- C99 起可以在初始化處宣告變數(
int i = 0),作用域僅限這個迴圈——建議永遠這樣寫。 - 三個部分都可以省略:
for (;;)是慣用的無窮迴圈寫法。 - 慣用範圍寫法
i = 0; i < n; i++(半開區間):執行剛好 n 次,i結束時等於n。
3. do-while:先執行、後檢查
int n = 942;
do {
printf("%d\n", n % 10); /* 至少會執行一次 */
n /= 10;
} while (n > 0);
本體保證至少執行一次。經典場景:逐位拆解數字(n 為 0 也要印一位)、選單「至少顯示一次」。
注意結尾的分號 ; 不能少。
4. 三種迴圈怎麼選?
| 情境 | 選擇 |
|---|---|
| 已知次數 / 走訪範圍 | for |
| 未知次數、可能一次都不執行 | while |
| 至少執行一次 | do-while |
5. 巢狀迴圈
外層每走一步,內層完整跑一輪。九九乘法表是最好的心智模型:
for (int i = 1; i <= 9; i++) { /* 外層:第幾列 */
for (int j = 1; j <= 9; j++) { /* 內層:第幾欄 */
printf("%d*%d=%2d ", i, j, i * j);
}
printf("\n"); /* 每列結束換行 */
}
總執行次數 = 外層次數 × 內層次數。三層以上就要警覺效能與可讀性。
6. break 與 continue
for (int i = 0; i < 100; i++) {
if (data[i] < 0) {
break; /* 遇到負數:整個迴圈立刻結束 */
}
if (data[i] % 2 != 0) {
continue; /* 奇數:跳過下面的程式,直接進入下一圈 */
}
printf("%d is a non-negative even number\n", data[i]);
}
break:跳出最內層的迴圈或 switch。continue:跳到本圈結尾(for 的話會先執行「推進」再檢查條件)。- 兩者都只影響一層——多層巢狀不會一次跳出去。
7. goto:為什麼少用?
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (found(i, j)) {
goto done; /* break 只能跳一層,goto 可以跳出全部 */
}
}
}
done:
printf("search finished\n");
goto 把控制流變成「任意跳躍」,濫用會讓程式無法推理(1968 年 Dijkstra 的名文
〈Go To Statement Considered Harmful〉即在批評此事)。但在 C 中有兩個公認的正當用途:
- 跳出多層巢狀迴圈(如上例)。
- 集中錯誤清理(
goto cleanup;——Linux 核心大量使用)。
規則:只往下跳、只跳到函式結尾附近的清理/收尾區,其他情況一律用結構化寫法。
範例程式
| 檔案 | 內容 | 編譯與執行 |
|---|---|---|
01_three_loops.c |
while / for / do-while 三種迴圈對照,含 K&R 攝氏華氏對照表 | cc -std=c11 -Wall -Wextra -o 01_three_loops 01_three_loops.c && ./01_three_loops |
02_nested_multiplication.c |
巢狀迴圈:九九乘法表、星號三角形、簡單圖形輸出 | cc -std=c11 -Wall -Wextra -o 02_nested_multiplication 02_nested_multiplication.c && ./02_nested_multiplication |
03_break_continue_prime.c |
break/continue 實戰:質數判斷與 2~100 質數表 | cc -std=c11 -Wall -Wextra -o 03_break_continue_prime 03_break_continue_prime.c && ./03_break_continue_prime |
04_goto_demo.c |
goto 的正當用途:跳出雙層迴圈、集中清理;並示範等價的結構化寫法 | cc -std=c11 -Wall -Wextra -o 04_goto_demo 04_goto_demo.c && ./04_goto_demo |
常見錯誤與陷阱
陷阱 1:忘記推進造成無窮迴圈
int i = 0;
while (i < 10) {
printf("%d\n", i); /* 少了 i++ → 永遠印 0 */
}
修正:確認迴圈內有讓條件「終將不成立」的變化;用 for 把推進寫進標頭更不容易漏。
陷阱 2:差一錯誤(off-by-one)
for (int i = 0; i <= n; i++) { ... } /* 想跑 n 次卻跑了 n+1 次 */
修正:養成半開區間習慣 i = 0; i < n。想想「柵欄與柱子」:n 段柵欄有 n+1 根柱子。
陷阱 3:for 標頭後面多打分號
for (int i = 0; i < 5; i++); /* 分號 = 空本體,迴圈空轉 5 次 */
{
printf("only once\n"); /* 區塊只執行一次 */
}
修正:刪掉分號。編譯器(-Wempty-body)多半會警告。
陷阱 4:unsigned 倒數到 0 的無窮迴圈
for (unsigned i = 10; i >= 0; i--) { ... } /* i >= 0 恆真! */
unsigned 減到 0 再減會環繞成極大值。修正:用 int 倒數,或改寫條件為 i-- > 0。
陷阱 5:浮點數當迴圈計數器
for (double x = 0.0; x != 1.0; x += 0.1) { ... } /* 可能永遠不等於 1.0 */
0.1 無法精確表示,累加十次不會剛好是 1.0。修正:用整數計數,需要浮點值時再換算:
for (int i = 0; i < 10; i++) { double x = i * 0.1; ... }。
練習題
- 基本題:分別用 while、for、do-while 印出 1 到 10,體會三種寫法的差異。
- 華氏轉攝氏(K&R 習題 1-4):印出華氏 0 到 300、間隔 20 的華氏→攝氏對照表(本單元範例是攝氏→華氏,方向相反)。
- 倒序與步進(K&R 習題 1-5):把溫度對照表改成「由高到低」印出;再寫一個只印偶數的 1~20 迴圈(兩種寫法:步進 2、或 continue 跳過奇數)。
- 圖形輸出:用巢狀迴圈印出 n=5 的直角三角形、倒三角形與金字塔(提示:每列先印空格再印星號)。
- 數字反轉:用 do-while 把 12345 反轉成 54321 印出(提示:% 10 取尾數、/ 10 去尾數)。想想為什麼 do-while 比 while 適合處理 n=0 的情況。
- 挑戰題(質數優化):把範例的質數判斷從「試除到 n-1」優化成「試除到 √n」,比較兩者對 n=1000003 的試除次數(提示:用
i * i <= n避免浮點運算與溢位問題則更佳)。
延伸閱讀
- K&R 2/e §1.3、§3.5–3.8;習題 1-4 ~ 1-6、3-4 ~ 3-6
- Dijkstra,〈Go To Statement Considered Harmful〉(CACM, 1968)
- Linux 核心程式碼風格文件中關於 goto 清理模式的段落
- 「循環不變式(loop invariant)」概念——寫出正確迴圈的思考工具
- 下一單元預告:函式——把程式拆成可重用的積木