本檔提供本單元練習題的完整詳解。先自己動手做:每題先只看「題目」, 寫完、編譯、執行後再對照「解題思路」與「參考解答」。 完整程式碼都在 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 各跑一次,確認三條路徑都被走過。

解題思路

  1. 分析問題:整數與 0 的關係只有三種——大於、小於、等於,互斥且完備。 這正是 else-if 階梯的典型場景。
  2. 選擇結構:三種情況寫成 if / else if / else。 最後的 else 不需要條件——前兩個都不成立,剩下的必然是零。
  3. 測資設計:好的測試讓每條分支至少執行一次。三條路徑 → 至少三筆測資 (−7、0、42)。「讓測資覆蓋所有分支」是本題想建立的習慣。
  4. 邊界: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)。 用四組資料覆蓋四個級距,並以表格形式輸出。注意條件的順序。

解題思路

  1. 公式:bmi = w / (h * h)。身高的平方要加括號——w / h * h 由左到右算, 會變成 (w/h)*h = w,完全錯掉。
  2. 分級用 else-if 而不是 switch:級距是範圍判斷,switch 只能比對離散值。
  3. 條件順序是本題核心:由小到大排(<18.5 → <24 → <27 → else)。 因為階梯有「前面都不成立」的隱含前提,第二格的 bmi < 24 實際語意是 18.5 <= bmi < 24——不必寫成 bmi >= 18.5 && bmi < 24。 如果順序打亂(例如先判斷 < 27),BMI 17 的人會被分到過重。
  4. 資料表示:級距名稱用 const char *category 接住, 判斷與輸出分離——printf 只寫一次,之後要改輸出格式只改一處。
  5. 測資設計:固定身高 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。

解題思路

  1. 為什麼用 switch:月份是離散整數值、分支固定——switch 的主場。
  2. fall-through 的正當用法:case 1: case 3: case 5:… 連續排列、中間不放任何敘述, 代表「這些值都執行下面同一段」。這是刻意共用,與「忘了 break」有本質區別。
  3. 分組:31 天七個月、30 天四個月、2 月自成一組。每組結尾必須 break, 否則 31 天組會掉進 30 天組。
  4. default 的價值:迴圈明明只跑 1~12,為什麼還寫 default?—— 防禦未來的修改(有人改了迴圈範圍)、讓「不可能的值」大聲失敗而不是沉默。
  5. 測試方式:不用手動輸入,直接 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'、'$' 覆蓋所有分類。

解題思路

  1. 依據:ASCII 中 'A'~'Z'、'a'~'z'、'0'~'9' 三段各自連續, 所以「屬於某段」可以寫成範圍比較:c >= 'A' && c <= 'Z'。 直接拿字元常數比大小,不要背 65、90 這些數字。
  2. 空白類是離散值不是連續段:空格、tab、換行三個值用 == 加 || 列舉。
  3. 順序與完備性:四個具體分類依序判斷,else 收「其他」。 分類彼此不重疊,順序其實可以互換——但把「最常見的」放前面是好習慣。
  4. 輸出的小難題:'\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) 執行,觀察兩版本輸出何時相同、何時不同。

解題思路

  1. 先理解規則:else 永遠配對「最近的、還沒有 else 的 if」,與縮排無關。 沒加大括號時,編譯器的解讀就是「內層版」。
  2. 把兩種語意都寫出來是本題的重點——用大括號讓意圖變成語法: - 內層版:if (a>0) { if (b>0) {...} else {...} }; - 外層版:if (a>0) { if (b>0) {...} } else {...}。 兩者差別只在一個大括號的位置,語意卻完全不同。
  3. 測資設計(預測輸出再執行): - (5, 3):兩版都走「both positive」→ 相同; - (5, -3):內層版說「a positive, b not」、外層版沉默 → 不同; - (-5, 3):內層版沉默、外層版說「a is not positive」→ 不同。
  4. 工程化:把兩個版本包成兩個函式(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 計算並印出結果。要求:

  1. 除以零時印出錯誤訊息,不能讓程式印出 inf 或 nan;
  2. 未知運算子(如 '?')由 default 回報錯誤;
  3. main 用寫死的測資覆蓋:四種運算、除以零、未知運算子,共六筆。

解題思路

  1. 選擇結構:運算子是離散的字元值(字元常數就是整數)→ switch (op) 直接比對。
  2. 先想清楚危險路徑:浮點除以 0 不會當機,會產生 inf/nan 然後安靜地污染下游—— 比當機更難除錯。所以 case '/' 內先檢查 b == 0.0,把錯誤在源頭擋下。 (對 0.0 用 == 判斷在這裡是安全的:我們問的是「是否恰好是零」這個明確狀態。)
  3. 未知運算子:default 不是可有可無的裝飾——它把「呼叫者傳錯東西」變成 看得見的錯誤訊息,而不是沉默吞掉。
  4. 函式化:把單筆計算包成 calculate,main 只負責餵測資。 六筆測資 = 四條正常路 + 兩條錯誤路,每條 switch 分支都被走到。
  5. 輸出格式:%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 污染下游 除法前檢查除數