本檔提供本單元練習題的完整詳解。先自己動手做:每題先只看「題目」,
寫完、編譯、執行後再對照「解題思路」與「參考解答」。
完整程式碼都在 exercises/ 資料夾,可直接編譯:
cc -std=c11 -Wall -Wextra -o /tmp/out exercises/ex1_three_loops.c && /tmp/out
練習 1:三種迴圈印 1 到 10(難度:基礎)
題目
分別用 while、for、do-while 印出 1 到 10(同一行、空格分隔),
三段輸出各占一行並標明迴圈種類。寫完後回答:三種寫法中,
「初始化、條件、推進」三要素各在哪裡?
解題思路
- 先抓住共同骨架:任何計數迴圈都有三要素——
初始化(
i = 1)、條件(i <= 10)、推進(i++)。三種迴圈只是把它們放在不同位置。 - while:初始化在迴圈前、條件在開頭、推進在本體內。 三件事分散三處——這正是「忘記推進造成無窮迴圈」好發於 while 的原因。
- for:三要素全部收進標頭
for (init; cond; step),一眼看完, 所以「已知次數」的迴圈永遠優先用 for。 - do-while:本體先執行一次、條件最後檢查。1~10 這種「至少一次」的情境結果相同, 差異要到「條件一開始就不成立」時才顯現(見練習 5)。
- 細節:for 的計數變數用 C99 寫法
for (int j = ...)宣告在標頭, 作用域只在迴圈內,不污染外面。
參考解答
#include <stdio.h>
int main(void)
{
/* --- while:初始化、條件、推進三件事分散在三處 --- */
printf("while : ");
int i = 1; /* 初始化在迴圈外 */
while (i <= 10) { /* 先檢查 */
printf("%d ", i);
i++; /* 推進寫在本體內,容易忘! */
}
printf("\n");
/* --- for:三件事集中在標頭,一眼看完 --- */
printf("for : ");
for (int j = 1; j <= 10; j++) {
printf("%d ", j);
}
printf("\n");
/* --- do-while:先做一次再問,條件寫在最後 --- */
printf("do-while: ");
int k = 1;
do {
printf("%d ", k);
k++;
} while (k <= 10); /* 注意結尾分號 */
printf("\n");
return 0;
}
(完整檔案:exercises/ex1_three_loops.c)
程式碼講解
- 三段輸出完全相同——證明三種迴圈可以互相改寫;選哪種是可讀性問題。
do { ... } while (k <= 10);結尾的分號不能少,少了是編譯錯誤。- while 版的
i++若忘記寫,條件永遠成立、1印到天荒地老—— 這是所有 C 初學者遲早要中一次的獎。
範例輸出
while : 1 2 3 4 5 6 7 8 9 10
for : 1 2 3 4 5 6 7 8 9 10
do-while: 1 2 3 4 5 6 7 8 9 10
練習 2:華氏轉攝氏對照表(難度:基礎,K&R 習題 1-4)
題目
單元範例印的是「攝氏→華氏」;請反過來印出華氏 0 到 300、間隔 20 的
華氏→攝氏對照表。公式:C = (5/9) × (F − 32)。
用 #define 定義上限、下限與間隔,輸出要對齊。
解題思路
- 反推公式:從
F = C × 9/5 + 32解出C = (5/9)(F − 32)。 - 迴圈設計:華氏值 0, 20, 40, …, 300——典型的「已知範圍 + 固定步進」,
用
for (fahr = LOWER; fahr <= UPPER; fahr += STEP)。 注意這裡要跑到含 300,所以條件是<=(半開區間慣例適用於「跑 n 次」, 對照表這種「含上限」場景用閉區間更自然)。 - 老陷阱新警惕:
5 / 9是整數除法 = 0,整張表會全是 0! 寫成5.0 / 9.0。計數變數fahr保持int(步進是整數),換算時自然升型。 - 符號常數:上下限與間隔是「表的規格」,用
#define集中在檔頭, 要改表的範圍時不必動迴圈邏輯——這正是 K&R §1.4 引入符號常數的動機。 - 對齊:
%10d、%10.1f固定欄寬,負數(−17.8)也不會破壞排版。
參考解答
#include <stdio.h>
#define LOWER 0 /* 表的下限 */
#define UPPER 300 /* 表的上限 */
#define STEP 20 /* 間隔 */
int main(void)
{
printf("Fahrenheit -> Celsius\n");
printf("---------------------\n");
/* 計數變數用 int(間隔固定 20),換算時才轉浮點 */
for (int fahr = LOWER; fahr <= UPPER; fahr += STEP) {
double celsius = 5.0 / 9.0 * (fahr - 32); /* 5.0/9.0 保住小數 */
printf("%10d %10.1f\n", fahr, celsius);
}
return 0;
}
(完整檔案:exercises/ex2_f_to_c.c)
程式碼講解
5.0 / 9.0 * (fahr - 32):乘除由左到右——先算 0.5556 再乘。fahr - 32是 int,但與 double 相乘時自動升型,不丟精度。- 0°F 對應 −17.8°C、32°F 對應 0°C(表裡看得到 20→−6.7、40→4.4 夾住它)、 212°F 對應 100°C——挑幾個已知點驗算是查表程式的基本驗證法。
#define之後改成STEP 10重編譯,整張表自動變密——「規格與邏輯分離」的甜頭。
範例輸出
Fahrenheit -> Celsius
---------------------
0 -17.8
20 -6.7
40 4.4
60 15.6
80 26.7
100 37.8
120 48.9
140 60.0
160 71.1
180 82.2
200 93.3
220 104.4
240 115.6
260 126.7
280 137.8
300 148.9
練習 3:倒序與步進(難度:基礎,K&R 習題 1-5)
題目
- 把溫度對照表改成由高到低印出(華氏 300 到 0,間隔 60)。
- 印出 1~20 之間的偶數,用兩種寫法各做一次:
(a) 步進 2;(b) 走訪 1~20、用
continue跳過奇數。
解題思路
- 倒數迴圈的三要素鏡像:起點變上限(300)、條件變
>= 0、推進變-= 60。 三個地方同時改,漏一個就是空迴圈或無窮迴圈。 - 倒數的著名地雷(README 陷阱 4):計數變數若是
unsigned,i >= 0恆真! 這裡用int就沒事——記住「倒數用有號整數」。 - 偶數寫法 (a):直接讓迴圈「只踩偶數」——起點 2、步進 2。 迴圈次數少一半,意圖也最清楚。
- 偶數寫法 (b):走訪全部、用
continue把奇數「濾掉」。 本題中 (a) 更好;但當「跳過的條件」複雜到無法用步進表達時(例如跳過質數), continue 是唯一解。兩種都要會,並理解取捨。 - continue 的語意:跳過本圈剩下的敘述,直接進入下一圈的推進與條件檢查。
參考解答
#include <stdio.h>
int main(void)
{
/* --- 第一部分:由高到低的溫度表 --- */
printf("Fahrenheit -> Celsius (descending)\n");
/* 倒數三要素:從上限開始、條件 >= 下限、每圈遞減 */
for (int fahr = 300; fahr >= 0; fahr -= 60) { /* 間隔放大到 60 節省版面 */
printf("%6d %8.1f\n", fahr, 5.0 / 9.0 * (fahr - 32));
}
/* --- 第二部分:1~20 的偶數,寫法一:步進 2 --- */
printf("\nevens by stepping 2 : ");
for (int i = 2; i <= 20; i += 2) { /* 起點就是第一個偶數 */
printf("%d ", i);
}
printf("\n");
/* --- 寫法二:走訪全部、用 continue 跳過奇數 --- */
printf("evens via continue : ");
for (int i = 1; i <= 20; i++) {
if (i % 2 != 0) {
continue; /* 奇數:跳過本圈剩餘部分 */
}
printf("%d ", i);
}
printf("\n");
return 0;
}
(完整檔案:exercises/ex3_reverse_even.c)
程式碼講解
- 兩種偶數寫法輸出完全相同——用輸出驗證等價性,之後重構程式時也是同樣手法。
continue版的 if 條件是「要跳過的情況」(奇數), 守衛式寫法讓主邏輯(printf)保持在最外層縮排。- 倒數表的驗證:第一列該是 300、最後一列該是 0,中間每列差 60。
範例輸出
Fahrenheit -> Celsius (descending)
300 148.9
240 115.6
180 82.2
120 48.9
60 15.6
0 -17.8
evens by stepping 2 : 2 4 6 8 10 12 14 16 18 20
evens via continue : 2 4 6 8 10 12 14 16 18 20
練習 4:圖形輸出(難度:進階)
題目
用巢狀迴圈印出 n = 5 的三種圖形:直角三角形、倒三角形、金字塔。
* ***** *
** **** ***
*** *** *****
**** ** *******
***** * *********
(提示:金字塔每列先印空格再印星號。)
解題思路
- 建立「列 × 欄」的心智模型:外層迴圈 = 第幾列(i), 內層迴圈 = 這一列要印幾個什麼字元。每種圖形的差別只在「第 i 列印什麼」的公式。
- 推公式(拿紙筆數!):
- 直角三角形:第 i 列印 i 顆星(i = 1..5);
- 倒三角形:第 1 列 5 顆、第 2 列 4 顆…→ 第 i 列印
N - i + 1顆; - 金字塔:觀察第 i 列——先有N - i個空格,接著奇數顆星 1, 3, 5, … →2i - 1顆。 - 驗算公式:代入邊界值——金字塔第 1 列:4 空格 + 1 星;第 5 列:0 空格 + 9 星。吻合。
- 換行的位置:內層迴圈負責「一列的內容」,
printf("\n")放在內層結束後、 外層之內——放錯位置是本題最常見的 bug。
參考解答
#include <stdio.h>
#define N 5
int main(void)
{
/* --- 直角三角形:第 i 列印 i 顆星 --- */
printf("right triangle:\n");
for (int i = 1; i <= N; i++) { /* 外層:第幾列 */
for (int j = 1; j <= i; j++) { /* 內層:印 i 顆星 */
printf("*");
}
printf("\n");
}
/* --- 倒三角形:第 i 列印 N - i + 1 顆星 --- */
printf("\ninverted triangle:\n");
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N - i + 1; j++) {
printf("*");
}
printf("\n");
}
/* --- 金字塔:每列先印空格再印星號
* 第 i 列(1 起算):N - i 個空格 + (2i - 1) 顆星 --- */
printf("\npyramid:\n");
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N - i; j++) { /* 前導空格:把星星推到中間 */
printf(" ");
}
for (int j = 1; j <= 2 * i - 1; j++) {
printf("*");
}
printf("\n");
}
return 0;
}
(完整檔案:exercises/ex4_patterns.c)
程式碼講解
- 金字塔的一列由兩個並排的內層迴圈組成(空格迴圈 + 星號迴圈)—— 內層迴圈不一定只有一個。
- 兩個內層迴圈都用
j,互不衝突:各自宣告在自己的 for 標頭,作用域不重疊。 - 把
N改成 8 重新編譯,三個圖形自動長大——公式對了,規模就是免費的。
範例輸出
right triangle:
*
**
***
****
*****
inverted triangle:
*****
****
***
**
*
pyramid:
*
***
*****
*******
*********
練習 5:數字反轉(難度:進階)
題目
用 do-while 把 12345 反轉成 54321 印出(提示:% 10 取尾數、/ 10 去尾數)。
用測資 12345、100、7、0 驗證,並回答:
為什麼處理 n = 0 時,do-while 比 while 更貼切?
解題思路
- 拆解演算法:反轉 = 不斷「取出最後一位、接到結果尾巴」:
-
digit = n % 10取出個位(12345 → 5); -reversed = reversed * 10 + digit——舊結果整體左移一位、空出的個位放新數字 (0→5→54→543→5432→54321); -n /= 10丟掉已處理的個位(12345 → 1234)。 - 迴圈選擇:n = 0 時它仍然「有一位數字要處理」(就是 0 本身)。
while (n > 0)一次都不執行(結果碰巧也是 0);do-while則忠實執行一次「處理一位」——語意上它才是對的: 任何整數至少有一位。 - 追蹤驗證(拿 12345 走一遍):
| 圈 | digit | reversed | n(處理後) |
|---|---|---|---|
| 1 | 5 | 5 | 1234 |
| 2 | 4 | 54 | 123 |
| 3 | 3 | 543 | 12 |
| 4 | 2 | 5432 | 1 |
| 5 | 1 | 54321 | 0 |
- 邊界觀察:
reverse(100) = 1——前導零在整數的世界自然消失,這不是 bug 而是數學。
參考解答
#include <stdio.h>
/* 反轉一個非負整數:12345 -> 54321 */
int reverse_number(int n)
{
int reversed = 0;
/* do-while:n 是 0 也會執行一次,反轉結果正確地是 0。
* 若用 while (n > 0),n = 0 時一次都不跑,
* 雖然結果碰巧也是 0,但「至少處理一位」的語意用 do-while 才貼切 */
do {
int digit = n % 10; /* 取出最後一位 */
reversed = reversed * 10 + digit; /* 舊結果左移一位、接上新位 */
n /= 10; /* 去掉最後一位 */
} while (n > 0);
return reversed;
}
int main(void)
{
int tests[] = { 12345, 100, 7, 0 };
int n = (int)(sizeof tests / sizeof tests[0]);
for (int i = 0; i < n; i++) {
printf("reverse(%5d) = %d\n", tests[i], reverse_number(tests[i]));
}
/* 觀察 100 -> 1:前導零在整數世界自然消失 */
printf("\nnote: reverse(100) = 1, leading zeros vanish in integers.\n");
return 0;
}
(完整檔案:exercises/ex5_reverse_digits.c)
程式碼講解
- 解答把演算法包成
reverse_number函式,方便用四筆測資重複呼叫—— 函式的完整介紹在下一單元,這裡先體驗「一次定義、多次使用」的好處。 reversed * 10 + digit是「逐位建數」的萬用模式, 單元 08 的my_atoi(字串轉整數)用的是同一條公式。- 侷限:極大的數(如 1999999999)反轉會超過
INT_MAX——溢位防護屬於進階話題, 此處測資刻意避開。
範例輸出
reverse(12345) = 54321
reverse( 100) = 1
reverse( 7) = 7
reverse( 0) = 0
note: reverse(100) = 1, leading zeros vanish in integers.
練習 6:質數判斷優化(難度:挑戰)
題目
質數判斷的天真寫法是「用 2 到 n−1 逐一試除」。請:
- 實作天真版,統計對
n = 1000003(質數)的試除次數; - 優化成「試除到 √n」——但不用浮點,改用條件
i * i <= n; - 印出兩版的判斷結果與試除次數,計算加速倍率。
解題思路
- 為什麼 √n 就夠(本題的數學核心):若 n = d × e(d ≤ e), 則 d ≤ √n(否則 d × e > √n × √n = n,矛盾)。 所以只要 √n 以內沒有因數,√n 以外也不可能有——搜尋範圍從 n−2 縮到 √n−1。
- 為什麼用
i * i <= n而不是i <= sqrt(n): - 不需要<math.h>與浮點運算; -sqrt的浮點誤差在邊界值可能差一(例如完全平方數); - 乘法比開根號快。 唯一要防的是i * i本身溢位——在比較前把它轉成long計算。 - 試除次數的預測:n = 1000003 是質數(最壞情況:整個範圍都要走完)—— 天真版試 2..1000002 共 1000001 次;√1000003 ≈ 1000.0015, 優化版試 2..1000 共 999 次。約 1000 倍差距。
- break 的角色:兩版都在「找到因數」的瞬間 break—— 合數的判斷通常很快結束;質數才要走完全程。
- 變數設計:
is_prime旗標先設 1(假設是質數),找到反例改 0;trials計數器統計試除次數,用long保險。
參考解答
#include <stdio.h>
int main(void)
{
int n = 1000003; /* 一個 7 位數的質數 */
/* --- 天真版:從 2 試除到 n-1 --- */
int is_prime_naive = 1; /* 先假設是質數 */
long trials_naive = 0; /* 試除次數統計 */
for (int i = 2; i < n; i++) {
trials_naive++;
if (n % i == 0) {
is_prime_naive = 0;
break; /* 找到因數就能下結論,提早離開 */
}
}
/* --- 優化版:只試除到 √n ---
* 原理:因數成對出現(d 與 n/d),兩者必有一個 <= √n,
* 所以 √n 以內都沒有因數的話,以上也不可能有。
* 用 i * i <= n 而不是 i <= sqrt(n):不碰浮點、不需 <math.h>;
* i*i 先轉 long 計算,防止接近 INT_MAX 時溢位。 */
int is_prime_fast = 1;
long trials_fast = 0;
for (int i = 2; (long)i * i <= n; i++) {
trials_fast++;
if (n % i == 0) {
is_prime_fast = 0;
break;
}
}
/* --- 結果對照 --- */
printf("n = %d\n\n", n);
printf("naive (up to n-1) : prime = %d, trial divisions = %ld\n",
is_prime_naive, trials_naive);
printf("fast (up to sqrt): prime = %d, trial divisions = %ld\n",
is_prime_fast, trials_fast);
printf("\nspeedup: %ld / %ld = about %ldx fewer divisions\n",
trials_naive, trials_fast, trials_naive / trials_fast);
return 0;
}
(完整檔案:exercises/ex6_prime_opt.c)
程式碼講解
(long)i * i <= n:先把i轉成 long 再乘,整條乘法在 long 域進行。 若寫i * i(int 乘 int),i 接近 46341 時就會有號溢位(UB)。 本題 n 不大所以踩不到,但寫防溢位的形式是免費的保險。- 兩版對同一個 n 得到相同結論(prime = 1),但成本差了約 1000 倍—— 演算法層面的優化遠勝任何微調。
- 進一步的優化方向(有興趣可自行延伸):先判 2 的倍數、之後只試奇數(再省一半)、 或試除改用已知質數表(埃拉托斯特尼篩法,中階單元的好題材)。
範例輸出
n = 1000003
naive (up to n-1) : prime = 1, trial divisions = 1000001
fast (up to sqrt): prime = 1, trial divisions = 999
speedup: 1000001 / 999 = about 1001x fewer divisions
常見錯誤解法對照
| 錯誤寫法 | 症狀 | 正解 |
|---|---|---|
while 本體忘記 i++ |
無窮迴圈 | 推進放進 for 標頭最保險 |
for (i = 0; i <= n; i++) 想跑 n 次 |
差一錯誤:多跑一次 | 半開區間 i < n |
for (...); 標頭後多分號 |
迴圈空轉、區塊只執行一次 | 刪分號(看 -Wempty-body 警告) |
for (unsigned i = 10; i >= 0; i--) |
i >= 0 恆真,無窮迴圈 |
倒數用 int,或 i-- > 0 |
5 / 9 * (fahr - 32) |
整數除法先變 0,整張表全 0 | 5.0 / 9.0 |
| 金字塔忘了前導空格 | 星星全部靠左,變直角三角形 | 每列先印 N - i 個空格 |
i <= sqrt(n) 判質數 |
引入浮點誤差與 math.h 依賴 | (long)i * i <= n |