本檔提供本單元練習題的完整詳解。先自己動手做:每題先只看「題目」, 寫完、編譯、執行後再對照「解題思路」與「參考解答」。 完整程式碼都在 exercises/ 資料夾,可直接編譯:

cc -std=c11 -Wall -Wextra -o /tmp/out exercises/ex1_arithmetic.c && /tmp/out

練習 1:五個算術運算子(難度:基礎)

題目

宣告 int a = 17, b = 5;,印出 a+b、a-b、a*b、a/b、a%b 五個結果, 先手算再執行驗證。加碼:印出 (a/b)*b + a%b,驗證它等於 a。

解題思路

  1. 先手算(這一步不可省):17+5=22、17−5=12、17×5=85; 除法與取餘要小心——17 / 5 是整數除法,商 3(0.4 被截掉);17 % 5 是餘數 2。
  2. 除法與取餘是一對:C 保證 (a/b)*b + a%b == a。用 3×5+2=17 驗證, 這條恆等式也是理解「負數除法/取餘」行為的鑰匙。
  3. 輸出裡想印 % 字元:格式字串中 % 是特殊字元,要寫 %%。 printf("a %% b") 印出 a % b。

參考解答

#include <stdio.h>

int main(void)
{
    int a = 17, b = 5;

    printf("a = %d, b = %d\n\n", a, b);
    printf("a + b = %d\n", a + b);   /* 22 */
    printf("a - b = %d\n", a - b);   /* 12 */
    printf("a * b = %d\n", a * b);   /* 85 */
    printf("a / b = %d\n", a / b);   /* 3:整數除法,商向零截斷 */
    printf("a %% b = %d\n", a % b);  /* 2:餘數。%% 才能印出 % 字元 */

    /* 驗算:除法與取餘的關係 a == (a/b)*b + (a%b) 永遠成立 */
    printf("\ncheck: (a/b)*b + a%%b = %d*%d + %d = %d\n",
           a / b, b, a % b, (a / b) * b + a % b);

    return 0;
}

(完整檔案:exercises/ex1_arithmetic.c)

程式碼講解

  • a / b 的結果向零截斷:17/5 = 3、-17/5 = -3(不是 −4,C99 起明確定義)。
  • a % b 的符號跟隨被除數:-17 % 5 = -2。
  • 恆等式 (a/b)*b + a%b == a 印出 17,與 a 一致——除法丟掉的部分正好被餘數補回。

範例輸出

a = 17, b = 5

a + b = 22
a - b = 12
a * b = 85
a / b = 3
a % b = 2

check: (a/b)*b + a%b = 3*5 + 2 = 17

練習 2:閏年判斷運算式(難度:進階)

題目

用 &&、|| 組合出一條「判斷 year 是否為閏年」的運算式: 能被 4 整除但不能被 100 整除,或能被 400 整除。 對 1900、2000、2024、2025 四個年份印出判斷結果(含運算式的值 0/1)。

預期:1900 不是(能被 100 整除卻不能被 400 整除)、2000 是、2024 是、2025 不是。

解題思路

  1. 把中文規則翻成邏輯,先拆出三個基本判斷: - 被 4 整除:year % 4 == 0 - 不被 100 整除:year % 100 != 0 - 被 400 整除:year % 400 == 0
  2. 照語意組合:「(A 且 B)或 C」→ (A && B) || C。 && 優先於 ||,所以內層括號其實可省,但寫出來意圖更清楚。
  3. 選測資的理由(好測資要覆蓋所有分支): - 2024:走 A && B 為真的路徑; - 1900:A 真、B 假、C 假 → 整體假(世紀年陷阱,正是規則存在的原因); - 2000:A && B 假(被 100 整除)但 C 真 → 整體真; - 2025:A 假,靠短路直接判定假。
  4. 輸出設計:邏輯運算式的值就是 int 的 0 或 1,直接印出, 再配 ?: 選字串讓輸出像句子。

參考解答

#include <stdio.h>

int main(void)
{
    int years[] = { 1900, 2000, 2024, 2025 };
    int n = (int)(sizeof years / sizeof years[0]);

    for (int i = 0; i < n; i++) {
        int year = years[i];

        /* 整條規則直接翻成邏輯運算式:
         * (被 4 整除 且 不被 100 整除) 或 (被 400 整除) */
        int is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);

        printf("%d is %sa leap year (expression value = %d)\n",
               year, is_leap ? "" : "NOT ", is_leap);
    }

    return 0;
}

(完整檔案:exercises/ex2_leap_year.c)

程式碼講解

  • is_leap 直接接住整條邏輯運算式的值(0 或 1)——判斷結果本身就是值, 不需要 if (...) is_leap = 1; else is_leap = 0; 這種冗長寫法。
  • 短路求值在此發揮效率:2025 算完 year % 4 == 0 為假之後, && 右邊與 || 右邊照樣要看——注意 || 左邊為假時仍會測右邊的 year % 400。
  • is_leap ? "" : "NOT ":用 ?: 選擇要插入的字串,一行搞定兩種輸出。

範例輸出

1900 is NOT a leap year (expression value = 0)
2000 is a leap year (expression value = 1)
2024 is a leap year (expression value = 1)
2025 is NOT a leap year (expression value = 0)

練習 3:平均值——整數除法 vs 浮點除法(難度:基礎)

題目

三科成績 88、73、95(皆為 int),分別用: (1) 整數除法、(2) cast 轉型、(3) 浮點字面值 3.0、(4)「先除再轉」的錯誤示範, 計算平均並印出,觀察並解釋差異。

解題思路

  1. 手算基準:88+73+95 = 256,256 ÷ 3 = 85.333…。
  2. 整數版錯在哪:sum / 3 兩邊都是 int → 整數除法 → 85,小數沒了。
  3. 兩種正解,原理相同——讓除法「至少一邊」是浮點: - (double)sum / 3:cast 的優先順序高於 /,sum 先變 256.0; - sum / 3.0:字面值本身就是 double。
  4. 關鍵陷阱:(double)(sum / 3)——括號讓整數除法先發生, 85 再轉成 85.0,為時已晚。cast 的位置決定一切。
  5. 驗證:四個版本一起印出對照,差異一目瞭然。

參考解答

#include <stdio.h>

int main(void)
{
    int s1 = 88, s2 = 73, s3 = 95;
    int sum = s1 + s2 + s3;                 /* 256 */

    /* 錯誤示範:sum 與 3 都是 int → 整數除法,小數被丟掉 */
    int avg_int = sum / 3;                  /* 256 / 3 = 85 */
    printf("integer division : %d / 3 = %d\n", sum, avg_int);

    /* 正解一:cast 讓除法的一邊變成 double */
    double avg1 = (double)sum / 3;
    printf("cast version     : (double)%d / 3 = %f\n", sum, avg1);

    /* 正解二:用浮點字面值 3.0 */
    double avg2 = sum / 3.0;
    printf("literal version  : %d / 3.0 = %.2f\n", sum, avg2);

    /* 常見錯誤:先做整數除法、再轉 double —— 為時已晚 */
    double too_late = (double)(sum / 3);
    printf("too-late cast    : (double)(%d / 3) = %.2f  (fraction lost)\n",
           sum, too_late);

    return 0;
}

(完整檔案:exercises/ex3_average.c)

程式碼講解

  • (double)sum / 3:讀法是 ((double)sum) / 3——cast 是一元運算子,優先順序高於除法。 左邊變 double 後,右邊的 3 也被隱式轉換成 3.0(usual arithmetic conversions)。
  • %f 預設印 6 位小數(85.333333);%.2f 控制成兩位(85.33)。
  • (double)(sum / 3) 印出 85.00——cast 放錯位置比不 cast 更難發現, 因為型別看起來全對、值卻悄悄錯了。

範例輸出

integer division : 256 / 3 = 85
cast version     : (double)256 / 3 = 85.333333
literal version  : 256 / 3.0 = 85.33
too-late cast    : (double)(256 / 3) = 85.00  (fraction lost)

練習 4:條件運算子 ?:(難度:進階)

題目

用 ?: 完成三件事並印出結果(用寫死的 a = 42, b = 17, c = 65 測試):

  • (a) 兩數取大值;
  • (b) 判斷奇偶,印出 odd / even;
  • (c) 巢狀 ?: 三數取最大值(做完想想:是不是 if/else 更可讀?)。

加碼:用 ?: 處理英文單複數(1 item / 3 items)。

解題思路

  1. ?: 的定位:它是「會回傳值的 if/else」。當你要的是二選一的值 (而不是二選一的動作)時,?: 讓程式更精簡。
  2. (a) 取大值:(a > b) ? a : b——條件真取 a、假取 b。這是 ?: 的教科書用法。
  3. (b) 奇偶:判斷 a % 2 != 0,然後選的不是數字而是字串 "odd" / "even", 直接塞進 printf 的 %s。?: 選什麼型別的值都行。
  4. (c) 三數取最大:分治思考——先比 a、b,「勝者」再與 c 比: (a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c)。 寫的時候用換行與縮排對齊兩個分支;讀的時候由外往內拆。 誠實的結論:兩層已是可讀性極限,三層以上請改 if/else 或拆函式。
  5. 單複數技巧:(n == 1) ? "" : "s"——輸出品質的小細節,K&R 也常用。

參考解答

#include <stdio.h>

int main(void)
{
    int a = 42, b = 17, c = 65;

    /* (a) 兩數取大值:?: 最典型的用途 */
    int max2 = (a > b) ? a : b;
    printf("max(%d, %d) = %d\n", a, b, max2);

    /* (b) 奇偶判斷:?: 選出要印的「字串」 */
    printf("%d is %s\n", a, (a % 2 != 0) ? "odd" : "even");
    printf("%d is %s\n", b, (b % 2 != 0) ? "odd" : "even");

    /* (c) 巢狀 ?: 三數取最大值:
     * 先比 a、b,勝者再與 c 比。可讀性已在邊緣——再複雜就改用 if/else */
    int max3 = (a > b) ? ((a > c) ? a : c)
                       : ((b > c) ? b : c);
    printf("max(%d, %d, %d) = %d\n", a, b, c, max3);

    /* 加碼:?: 處理單複數,輸出訊息更自然 */
    int items = 1;
    printf("You have %d item%s.\n", items, (items == 1) ? "" : "s");
    items = 3;
    printf("You have %d item%s.\n", items, (items == 1) ? "" : "s");

    return 0;
}

(完整檔案:exercises/ex4_ternary.c)

程式碼講解

  • ?: 的優先順序很低(只高於指定與逗號),但條件部分習慣加括號,一眼看清楚邊界。
  • 巢狀版的排版:兩個分支各占一行、? 與 : 對齊——排版是巢狀 ?: 可讀性的生命線。
  • 42 對 17:max2 = 42;三數再與 65 比:max3 = 65。手算與輸出一致。
  • 易錯處:?: 兩個分支的型別要相容(一邊 int、一邊字串會編譯錯誤)。

範例輸出

max(42, 17) = 42
42 is even
17 is odd
max(42, 17, 65) = 65
You have 1 item.
You have 3 items.

練習 5:優先順序偵探(難度:進階)

題目

先不執行程式,手算下面三條運算式的值,再寫程式驗證:

  1. 2 + 3 * 4 % 5
  2. 10 > 9 > 8(特別解釋:為什麼結果不是「數學上」的真?)
  3. 1 << 2 + 1(提示:+ 的優先順序比 << 高)

解題思路

  1. 案例一:* 與 % 同級且高於 +,同級由左到右: 3 * 4 = 12 → 12 % 5 = 2 → 2 + 2 = 4。
  2. 案例二:C 沒有數學的連鎖比較!關係運算子由左到右結合: 10 > 9 先算出 1(真),接著 1 > 8 為假 → 整條式子是 0。 想表達「9 介於 8 和 10 之間」必須寫 (10 > 9) && (9 > 8)。 注意:新版 Apple clang 直接把 10 > 9 > 8 這種寫法當成錯誤擋下 (chained comparison),逼你把意圖寫清楚——這是編譯器在救你。
  3. 案例三:出乎多數人意料,+ 高於 <<: 1 << 2 + 1 是 1 << (2 + 1) = 1 << 3 = 8,不是 (1 << 2) + 1 = 5。 移位運算的優先順序是歷史包袱,永遠加括號。 clang 對此也有警告(-Wshift-op-parentheses),所以參考解答直接寫出加括號的兩種語意。
  4. 方法論:手算 → 預測 → 執行對答案。答案不同時,回頭查優先順序表—— 這個「預測再驗證」的迴路是把優先順序表內化的最快方法。

參考解答

#include <stdio.h>

int main(void)
{
    /* 案例一:* 與 % 同級(高於 +),由左到右結合
     * 2 + 3 * 4 % 5 = 2 + ((3 * 4) % 5) = 2 + (12 % 5) = 2 + 2 = 4 */
    printf("2 + 3 * 4 %% 5 = %d\n", 2 + 3 * 4 % 5);

    /* 案例二:關係運算子由左到右結合
     * 10 > 9 > 8 其實是 (10 > 9) > 8 = 1 > 8 = 0
     * C 沒有數學的「連鎖比較」!想表達區間要寫 (x > a && x < b)。
     * 新版 clang 甚至直接把不加括號的連鎖比較當成錯誤擋下來,
     * 所以這裡明確寫出編譯器的解讀方式。 */
    printf("10 > 9 > 8    = %d  (parsed as (10 > 9) > 8)\n", (10 > 9) > 8);
    printf("(10 > 9) && (9 > 8) = %d  (what you probably meant)\n",
           (10 > 9) && (9 > 8));

    /* 案例三:+ 的優先順序高於 <<
     * 1 << 2 + 1 其實是 1 << (2 + 1) = 1 << 3 = 8(不是 4 + 1 = 5!)
     * 直接寫 1 << 2 + 1 的話,clang 甚至會警告要你加括號
     * (-Wshift-op-parentheses)——所以這裡把編譯器的理解寫明。 */
    printf("1 << 2 + 1    = %d  (parsed as 1 << (2 + 1))\n", 1 << (2 + 1));
    printf("(1 << 2) + 1  = %d  (if you wanted shift first)\n", (1 << 2) + 1);

    return 0;
}

(完整檔案:exercises/ex5_precedence.c)

程式碼講解

  • 案例二印出 0:關係運算子的結果是 int 的 0/1,1 > 8 當然是假。 「布林值可以再參與比較」是連鎖比較陷阱的根源。
  • 案例三印出 8 與 5 對照——同樣三個符號,括號位置不同、答案不同。
  • 實務守則:位元與移位運算式,一律加括號;被編譯器警告時不要壓警告,改寫程式。

範例輸出

2 + 3 * 4 % 5 = 4
10 > 9 > 8    = 0  (parsed as (10 > 9) > 8)
(10 > 9) && (9 > 8) = 1  (what you probably meant)
1 << 2 + 1    = 8  (parsed as 1 << (2 + 1))
(1 << 2) + 1  = 5  (if you wanted shift first)

練習 6:i++ 與 ++i 的行為差異(難度:挑戰)

題目

寫程式示範 i++(後置)與 ++i(前置)在三種情境的行為:

  • (a) 單獨成一行的敘述;
  • (b) 出現在指定運算的右邊;
  • (c) 當作函式引數。

最後用註解寫出至少兩個未定義行為的寫法並說明原因(不要真的執行)。

解題思路

  1. 一句話抓住差異:兩者都會把變數 +1;差別只在運算式本身的值—— 後置 i++ 的值是「舊值」,前置 ++i 的值是「新值」。
  2. (a) 單獨敘述:運算式的值沒人使用,所以兩者完全等價。 結論:單獨用時選哪個都對,全專案風格一致即可。
  3. (b) 指定右邊:值被接住了,差異現形—— post = x++ → post 拿舊值 5,x 變 6;pre = ++y → y 先變 6,pre 拿 6。
  4. (c) 函式引數:規則同 (b)。設計一個 show(int value) 函式把收到的值印出來, 就能「觀測」傳進去的是舊值還是新值:show(m++) 收到 10、show(++n) 收到 11。
  5. 未定義行為的邊界:一對定序點之間,同一變數「改了又讀」或「改兩次」就是 UB: - i = i++;(指定與遞增都在改 i) - printf("%d %d", i, i++);(引數求值順序未定,且讀寫同一變數) 這些寫在註解裡警示即可——示範 UB 的正確方式就是不執行它。

參考解答

#include <stdio.h>

/* 小工具:把收到的值原樣印出並回傳,用來觀察「傳進來的是什麼」 */
int show(int value)
{
    printf("    show() received %d\n", value);
    return value;
}

int main(void)
{
    /* --- (a) 單獨成一行:i++ 與 ++i 完全等價 --- */
    int i = 5, j = 5;
    i++;
    ++j;
    printf("(a) after i++; i = %d,  after ++j; j = %d  (identical)\n\n", i, j);

    /* --- (b) 出現在指定的右邊:差異現形 --- */
    int x = 5, y = 5;
    int post = x++;     /* 後置:先取「舊值」給 post,x 再變 6 */
    int pre  = ++y;     /* 前置:y 先變 6,再把「新值」給 pre */
    printf("(b) post = x++  -> post = %d, x = %d\n", post, x);
    printf("    pre  = ++y  -> pre  = %d, y = %d\n\n", pre, y);

    /* --- (c) 當函式引數:規則與 (b) 相同 --- */
    int m = 10, n = 10;
    printf("(c) calling show(m++) with m = 10:\n");
    show(m++);          /* 傳舊值 10,呼叫後 m = 11 */
    printf("    after the call, m = %d\n", m);

    printf("    calling show(++n) with n = 10:\n");
    show(++n);          /* n 先變 11,傳新值 11 */
    printf("    after the call, n = %d\n\n", n);

    /* --- 未定義行為示範(只說不做!) ---
     *
     *   i = i++;                  // 同一運算式對 i 又讀又寫:UB
     *   printf("%d %d", i, i++);  // 引數求值順序未定 + 讀寫衝突:UB
     *   a[i] = i++;               // 同上:UB
     *
     * 規則:兩個「相鄰定序點」之間,同一變數最多修改一次,
     * 且若有修改,就不能再讀它(除了為了算出新值的那次讀取)。 */
    printf("UB examples like \"i = i++;\" are shown in comments only.\n");

    return 0;
}

(完整檔案:exercises/ex6_incdec.c)

程式碼講解

  • show 函式是「觀測儀器」:把引數印出來,就能看到呼叫當下傳入的值。 函式的完整介紹在單元 06,這裡只需看懂「傳進去什麼、印什麼」。
  • (b) 的輸出 post = 5, x = 6 vs pre = 6, y = 6 是整題的核心對照。
  • 為什麼 UB 不能「執行看看」:UB 的行為隨編譯器、最佳化等級而變, 「這次跑對了」不代表什麼。看到這類寫法一律重構成兩行。

範例輸出

(a) after i++; i = 6,  after ++j; j = 6  (identical)

(b) post = x++  -> post = 5, x = 6
    pre  = ++y  -> pre  = 6, y = 6

(c) calling show(m++) with m = 10:
    show() received 10
    after the call, m = 11
    calling show(++n) with n = 10:
    show() received 11
    after the call, n = 11

UB examples like "i = i++;" are shown in comments only.

常見錯誤解法對照

錯誤寫法 症狀 正解
if (x = 5) 恆真;-Wall 警告 if (x == 5)
double avg = (a+b+c) / 3; 整數除法先截斷 / 3.0 或 (double)(a+b+c) / 3
(double)(sum / 3) cast 太晚,小數已丟 (double)sum / 3
flags & MASK == 0 實為 flags & (MASK==0) (flags & MASK) == 0
10 > 9 > 8 連鎖比較 新版 clang 直接報錯;語意也不是數學區間 (10 > 9) && (9 > 8)
i = i++; 未定義行為 拆成 i++; 一行完成