學習目標

  • 熟練三種迴圈: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 中有兩個公認的正當用途:

  1. 跳出多層巢狀迴圈(如上例)。
  2. 集中錯誤清理(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; ... }。

練習題

  1. 基本題:分別用 while、for、do-while 印出 1 到 10,體會三種寫法的差異。
  2. 華氏轉攝氏(K&R 習題 1-4):印出華氏 0 到 300、間隔 20 的華氏→攝氏對照表(本單元範例是攝氏→華氏,方向相反)。
  3. 倒序與步進(K&R 習題 1-5):把溫度對照表改成「由高到低」印出;再寫一個只印偶數的 1~20 迴圈(兩種寫法:步進 2、或 continue 跳過奇數)。
  4. 圖形輸出:用巢狀迴圈印出 n=5 的直角三角形、倒三角形與金字塔(提示:每列先印空格再印星號)。
  5. 數字反轉:用 do-while 把 12345 反轉成 54321 印出(提示:% 10 取尾數、/ 10 去尾數)。想想為什麼 do-while 比 while 適合處理 n=0 的情況。
  6. 挑戰題(質數優化):把範例的質數判斷從「試除到 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)」概念——寫出正確迴圈的思考工具
  • 下一單元預告:函式——把程式拆成可重用的積木