本檔提供本單元練習題的完整詳解。先自己動手做:每題先只看「題目」,
寫完、編譯、執行後再對照「解題思路」與「參考解答」。
完整程式碼都在 exercises/ 資料夾,可直接編譯:
cc -std=c11 -Wall -Wextra -o /tmp/out exercises/ex1_sign.c && /tmp/out
練習 1:正負零判斷(難度:基礎)
題目
用 if / else-if / else 判斷一個整數是 positive、negative 還是 zero。
用寫死的測資 -7、0、42 各跑一次,確認三條路徑都被走過。
解題思路
- 分析問題:整數與 0 的關係只有三種——大於、小於、等於,互斥且完備。 這正是 else-if 階梯的典型場景。
- 選擇結構:三種情況寫成
if / else if / else。 最後的else不需要條件——前兩個都不成立,剩下的必然是零。 - 測資設計:好的測試讓每條分支至少執行一次。三條路徑 → 至少三筆測資 (−7、0、42)。「讓測資覆蓋所有分支」是本題想建立的習慣。
- 邊界:0 是唯一的邊界值,必須明確測到。
參考解答
#include <stdio.h>
int main(void)
{
/* 三個測資剛好覆蓋三條路徑 */
int tests[] = { -7, 0, 42 };
int n = (int)(sizeof tests / sizeof tests[0]);
for (int i = 0; i < n; i++) {
int x = tests[i];
/* 三種情況互斥:else-if 階梯保證只走一條路 */
if (x > 0) {
printf("%3d is positive\n", x);
} else if (x < 0) {
printf("%3d is negative\n", x);
} else {
printf("%3d is zero\n", x); /* 不大於 0 也不小於 0,只剩 0 */
}
}
return 0;
}
(完整檔案:exercises/ex1_sign.c)
程式碼講解
- 階梯「由上而下、first match wins」:42 進第一格就結束,不會再看後面的條件。
- 最後的
else寫x == 0的判斷也可以,但多餘的條件反而模糊語意—— 「以上皆非」本身就是資訊。 %3d讓輸出靠右對齊 3 格,-號也算一格。
範例輸出
-7 is negative
0 is zero
42 is positive
練習 2:BMI 分級(難度:基礎)
題目
BMI = 體重(kg) ÷ 身高(m)²。用寫死的身高體重資料計算 BMI, 以 else-if 階梯分級:過輕(<18.5)、正常(<24)、過重(<27)、肥胖(≥27)。 用四組資料覆蓋四個級距,並以表格形式輸出。注意條件的順序。
解題思路
- 公式:
bmi = w / (h * h)。身高的平方要加括號——w / h * h由左到右算, 會變成(w/h)*h = w,完全錯掉。 - 分級用 else-if 而不是 switch:級距是範圍判斷,switch 只能比對離散值。
- 條件順序是本題核心:由小到大排(<18.5 → <24 → <27 → else)。
因為階梯有「前面都不成立」的隱含前提,第二格的
bmi < 24實際語意是18.5 <= bmi < 24——不必寫成bmi >= 18.5 && bmi < 24。 如果順序打亂(例如先判斷< 27),BMI 17 的人會被分到過重。 - 資料表示:級距名稱用
const char *category接住, 判斷與輸出分離——printf 只寫一次,之後要改輸出格式只改一處。 - 測資設計:固定身高 1.70,挑四個體重讓 BMI 落在四個級距 (50→17.3、62→21.4、72→24.9、85→29.4)。
參考解答
#include <stdio.h>
int main(void)
{
/* 四組測資,覆蓋四個級距 */
double heights[] = { 1.70, 1.70, 1.70, 1.70 };
double weights[] = { 50.0, 62.0, 72.0, 85.0 };
int n = (int)(sizeof weights / sizeof weights[0]);
printf("height weight BMI category\n");
printf("------ ------ ----- --------\n");
for (int i = 0; i < n; i++) {
double h = heights[i];
double w = weights[i];
double bmi = w / (h * h);
/* 條件由小到大排:每個分支隱含「前面的都不成立」,
* 所以 < 24 實際上代表 18.5 <= bmi < 24 */
const char *category;
if (bmi < 18.5) {
category = "underweight";
} else if (bmi < 24.0) {
category = "normal";
} else if (bmi < 27.0) {
category = "overweight";
} else {
category = "obese";
}
printf("%5.2f %5.1f %5.2f %s\n", h, w, bmi, category);
}
return 0;
}
(完整檔案:exercises/ex2_bmi.c)
程式碼講解
w / (h * h):先算平方再除。括號不是裝飾,是正確性。const char *category:指向字串常值的指標宣告成const(單元 08 詳談), 這裡先學「把選擇結果存變數、輸出集中一處」的模式。- 每個分支都只設定變數不直接 printf——「決策」與「動作」分離是可維護性的基本功。
範例輸出
height weight BMI category
------ ------ ----- --------
1.70 50.0 17.30 underweight
1.70 62.0 21.45 normal
1.70 72.0 24.91 overweight
1.70 85.0 29.41 obese
練習 3:switch 月份天數(難度:進階)
題目
給定月份 1~12(用迴圈跑完全部),以 switch 印出該月天數:
1、3、5、7、8、10、12 月是 31 天;4、6、9、11 月是 30 天;2 月印 28 (29 in leap years)。
用 fall-through 讓同天數的月份共用同一段程式,並保留 default。
解題思路
- 為什麼用 switch:月份是離散整數值、分支固定——switch 的主場。
- fall-through 的正當用法:
case 1: case 3: case 5:… 連續排列、中間不放任何敘述, 代表「這些值都執行下面同一段」。這是刻意共用,與「忘了 break」有本質區別。 - 分組:31 天七個月、30 天四個月、2 月自成一組。每組結尾必須
break, 否則 31 天組會掉進 30 天組。 - default 的價值:迴圈明明只跑 1~12,為什麼還寫 default?—— 防禦未來的修改(有人改了迴圈範圍)、讓「不可能的值」大聲失敗而不是沉默。
- 測試方式:不用手動輸入,直接
for (month = 1..12)全部印一遍,一眼驗收。
參考解答
#include <stdio.h>
int main(void)
{
for (int month = 1; month <= 12; month++) {
switch (month) {
/* 31 天的月份:七個 case 共用同一段(刻意 fall-through) */
case 1: case 3: case 5: case 7: case 8: case 10: case 12:
printf("month %2d has 31 days\n", month);
break;
/* 30 天的月份 */
case 4: case 6: case 9: case 11:
printf("month %2d has 30 days\n", month);
break;
/* 二月:平年 28、閏年 29 */
case 2:
printf("month %2d has 28 days (29 in leap years)\n", month);
break;
/* month 只會是 1~12,但 default 留著攔截「不可能的值」是好習慣 */
default:
printf("month %2d is invalid\n", month);
break;
}
}
return 0;
}
(完整檔案:exercises/ex3_month_days.c)
程式碼講解
case 1: case 3: ...排成一行是慣用排版:視覺上一眼看出「這是一組」。- 空 case 之間 fall-through 不需要註解;有程式碼的 case 要掉落才必須加
/* fall through */。 default也以break結尾——雖然它在最後不會掉落,但保持一致風格, 日後有人在下面加 case 時不會出事。
範例輸出
month 1 has 31 days
month 2 has 28 days (29 in leap years)
month 3 has 31 days
month 4 has 30 days
month 5 has 31 days
month 6 has 30 days
month 7 has 31 days
month 8 has 31 days
month 9 has 30 days
month 10 has 31 days
month 11 has 30 days
month 12 has 31 days
練習 4:字元分類(難度:進階)
題目
不使用 <ctype.h>,給定一個 char c,用 else-if 階梯判斷它是:
大寫字母、小寫字母、數字、空白(空格/tab/換行)或其他。
用測資 'G'、'q'、'7'、' '、'\t'、'\n'、'$' 覆蓋所有分類。
解題思路
- 依據:ASCII 中
'A'~'Z'、'a'~'z'、'0'~'9'三段各自連續, 所以「屬於某段」可以寫成範圍比較:c >= 'A' && c <= 'Z'。 直接拿字元常數比大小,不要背 65、90 這些數字。 - 空白類是離散值不是連續段:空格、tab、換行三個值用
==加||列舉。 - 順序與完備性:四個具體分類依序判斷,
else收「其他」。 分類彼此不重疊,順序其實可以互換——但把「最常見的」放前面是好習慣。 - 輸出的小難題:
'\t'、'\n'印出來是看不見的,輸出會亂掉。 解法:印分類之前,先把這幾個特殊字元翻譯成'\t'、'\n'的字面樣子。 「先處理顯示、再處理邏輯」讓兩段程式各自單純。
參考解答
#include <stdio.h>
int main(void)
{
/* 測資涵蓋所有分類 */
char tests[] = { 'G', 'q', '7', ' ', '\t', '\n', '$' };
int n = (int)(sizeof tests / sizeof tests[0]);
for (int i = 0; i < n; i++) {
char c = tests[i];
/* 空白類字元不能直接印出來,先準備可顯示的名稱 */
if (c == ' ') {
printf("' ' : ");
} else if (c == '\t') {
printf("'\\t' : ");
} else if (c == '\n') {
printf("'\\n' : ");
} else {
printf("'%c' : ", c);
}
/* 分類本體:靠 ASCII 各段連續的性質做範圍比較 */
if (c >= 'A' && c <= 'Z') {
printf("uppercase letter\n");
} else if (c >= 'a' && c <= 'z') {
printf("lowercase letter\n");
} else if (c >= '0' && c <= '9') {
printf("digit\n");
} else if (c == ' ' || c == '\t' || c == '\n') {
printf("whitespace\n");
} else {
printf("other\n");
}
}
return 0;
}
(完整檔案:exercises/ex4_char_class.c)
程式碼講解
c >= 'A' && c <= 'Z':兩個條件都要寫完整。 寫成'A' <= c <= 'Z'是連鎖比較陷阱(單元 03 練習 5),語意完全不同。- 想印出
\t這兩個字元本身,字串裡要寫\\t(反斜線自己也要跳脫)。 - 這支程式其實就是
<ctype.h>中isupper、islower、isdigit、isspace的手工版——之後改用標準函式時,你會確切知道它們在做什麼。
範例輸出
'G' : uppercase letter
'q' : lowercase letter
'7' : digit
' ' : whitespace
'\t' : whitespace
'\n' : whitespace
'$' : other
練習 5:懸掛 else 排雷(難度:進階)
題目
觀念講解中的懸掛 else 範例裡,else 實際配對的是內層 if。
請用大括號寫出兩種不同語意的版本:
- 版本一:else 屬於內層 if(
a > 0且b <= 0時說「a positive, b not」); - 版本二:else 屬於外層 if(
a <= 0時說「a is not positive」)。
對三組測資 (5, 3)、(5, -3)、(-5, 3) 執行,觀察兩版本輸出何時相同、何時不同。
解題思路
- 先理解規則:
else永遠配對「最近的、還沒有 else 的 if」,與縮排無關。 沒加大括號時,編譯器的解讀就是「內層版」。 - 把兩種語意都寫出來是本題的重點——用大括號讓意圖變成語法:
- 內層版:
if (a>0) { if (b>0) {...} else {...} }; - 外層版:if (a>0) { if (b>0) {...} } else {...}。 兩者差別只在一個大括號的位置,語意卻完全不同。 - 測資設計(預測輸出再執行):
-
(5, 3):兩版都走「both positive」→ 相同; -(5, -3):內層版說「a positive, b not」、外層版沉默 → 不同; -(-5, 3):內層版沉默、外層版說「a is not positive」→ 不同。 - 工程化:把兩個版本包成兩個函式(
inner_else、outer_else), 同一組測資餵給兩個函式對照——比複製貼上兩份 main 乾淨得多。
參考解答
#include <stdio.h>
/* 語意一:else 屬於「內層」if —— 這正是不加大括號時 C 的實際解讀 */
void inner_else(int a, int b)
{
if (a > 0) {
if (b > 0) {
printf(" inner-else version: both positive\n");
} else {
printf(" inner-else version: a positive, b not\n");
}
}
/* a <= 0 時整段沉默 */
}
/* 語意二:else 屬於「外層」if —— 縮排騙人時你「以為」的語意 */
void outer_else(int a, int b)
{
if (a > 0) {
if (b > 0) {
printf(" outer-else version: both positive\n");
}
/* a > 0 且 b <= 0 時沉默 */
} else {
printf(" outer-else version: a is not positive\n");
}
}
int main(void)
{
/* 三組測資:兩正 / a正b負 / a負 —— 兩版本只在後兩組表現不同 */
int as[] = { 5, 5, -5 };
int bs[] = { 3, -3, 3 };
for (int i = 0; i < 3; i++) {
printf("a = %d, b = %d:\n", as[i], bs[i]);
inner_else(as[i], bs[i]);
outer_else(as[i], bs[i]);
printf("\n");
}
printf("rule: an else binds to the NEAREST if without an else.\n");
printf("braces make your intent explicit -- always use them.\n");
return 0;
}
(完整檔案:exercises/ex5_dangling_else.c)
程式碼講解
- 輸出驗證了預測:
(5, 3)兩行都出現;(5, -3)只有內層版發聲;(-5, 3)只有外層版發聲。同一段縮排、不同大括號,行為完全不同。 - 不加大括號的原始寫法,編譯器會照「內層版」執行;
clang 還會給
-Wdangling-else警告提醒你縮排與語意不符。 - 實務結論永遠一句話:巢狀 if 一律加大括號,讓語意由語法保證而不是靠縮排祈禱。
範例輸出
a = 5, b = 3:
inner-else version: both positive
outer-else version: both positive
a = 5, b = -3:
inner-else version: a positive, b not
a = -5, b = 3:
outer-else version: a is not positive
rule: an else binds to the NEAREST if without an else.
braces make your intent explicit -- always use them.
練習 6:簡易計算機(難度:挑戰)
題目
寫函式 void calculate(double a, char op, double b):依運算子字元(+ - * /)
用 switch 計算並印出結果。要求:
- 除以零時印出錯誤訊息,不能讓程式印出
inf或nan; - 未知運算子(如
'?')由default回報錯誤; main用寫死的測資覆蓋:四種運算、除以零、未知運算子,共六筆。
解題思路
- 選擇結構:運算子是離散的字元值(字元常數就是整數)→
switch (op)直接比對。 - 先想清楚危險路徑:浮點除以 0 不會當機,會產生
inf/nan然後安靜地污染下游—— 比當機更難除錯。所以case '/'內先檢查b == 0.0,把錯誤在源頭擋下。 (對 0.0 用==判斷在這裡是安全的:我們問的是「是否恰好是零」這個明確狀態。) - 未知運算子:
default不是可有可無的裝飾——它把「呼叫者傳錯東西」變成 看得見的錯誤訊息,而不是沉默吞掉。 - 函式化:把單筆計算包成
calculate,main 只負責餵測資。 六筆測資 = 四條正常路 + 兩條錯誤路,每條 switch 分支都被走到。 - 輸出格式:
%g自動去掉多餘的零(12而非12.000000),適合計算機這種輸出。
參考解答
#include <stdio.h>
/* 把「一筆計算」封裝成函式,方便用多組測資重複呼叫 */
void calculate(double a, char op, double b)
{
switch (op) {
case '+':
printf("%g %c %g = %g\n", a, op, b, a + b);
break;
case '-':
printf("%g %c %g = %g\n", a, op, b, a - b);
break;
case '*':
printf("%g %c %g = %g\n", a, op, b, a * b);
break;
case '/':
/* 浮點除以 0 不會當機,而是產生 inf/nan —— 更危險,
* 因為錯誤會安靜地往下游擴散。先擋下來。 */
if (b == 0.0) {
printf("%g %c %g : error, division by zero\n", a, op, b);
} else {
printf("%g %c %g = %g\n", a, op, b, a / b);
}
break;
default:
/* switch 的 default 當「以上皆非」的守門員 */
printf("%g %c %g : error, unknown operator '%c'\n", a, op, b, op);
break;
}
}
int main(void)
{
/* 測資覆蓋:四種運算、除以零、未知運算子 */
calculate(12.0, '+', 7.5);
calculate(12.0, '-', 7.5);
calculate(12.0, '*', 7.5);
calculate(12.0, '/', 7.5);
calculate(12.0, '/', 0.0);
calculate(12.0, '?', 7.5);
return 0;
}
(完整檔案:exercises/ex6_calculator.c)
程式碼講解
switch (op)用字元當條件完全合法——字元常數本質是整數常數。case '/'裡面可以放if/else:case 的本體就是普通敘述序列, 複雜到一個程度就抽成函式。- 每個 case(含 default)都以
break結尾——這支程式若漏掉case '+'的 break, 加法算完會接著印減法結果,一次示範 fall-through 為什麼危險。 - 浮點的
b == 0.0只在「檢查確切狀態」時使用;比較計算結果仍要用容忍度 (單元 02 陷阱 5)。
範例輸出
12 + 7.5 = 19.5
12 - 7.5 = 4.5
12 * 7.5 = 90
12 / 7.5 = 1.6
12 / 0 : error, division by zero
12 ? 7.5 : error, unknown operator '?'
常見錯誤解法對照
| 錯誤寫法 | 症狀 | 正解 |
|---|---|---|
if (x > 0); 多打分號 |
區塊無條件執行 | 刪掉分號;開 -Wall 看警告 |
| BMI 條件由大到小排 | 低 BMI 被高級距攔截 | 由小到大排,或每格寫完整區間 |
| switch 忘記 break | 上一組的輸出「掉」進下一組 | 每組結尾 break; |
if (c == 'y' \|\| 'Y') |
恆真('Y' 非零) |
c == 'y' \|\| c == 'Y' |
| 巢狀 if 不加大括號 | else 配對到內層 if,縮排騙人 | 一律加大括號 |
| 浮點除以零不檢查 | 輸出 inf/nan 污染下游 | 除法前檢查除數 |