本檔提供本單元練習題的完整詳解。先自己動手做:每題先只看「題目」,
寫完、編譯、執行後再對照「解題思路」與「參考解答」。
完整程式碼都在 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。
解題思路
- 先手算(這一步不可省):17+5=22、17−5=12、17×5=85;
除法與取餘要小心——
17 / 5是整數除法,商 3(0.4 被截掉);17 % 5是餘數 2。 - 除法與取餘是一對:C 保證
(a/b)*b + a%b == a。用 3×5+2=17 驗證, 這條恆等式也是理解「負數除法/取餘」行為的鑰匙。 - 輸出裡想印
%字元:格式字串中%是特殊字元,要寫%%。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 不是。
解題思路
- 把中文規則翻成邏輯,先拆出三個基本判斷:
- 被 4 整除:
year % 4 == 0- 不被 100 整除:year % 100 != 0- 被 400 整除:year % 400 == 0 - 照語意組合:「(A 且 B)或 C」→
(A && B) || C。&&優先於||,所以內層括號其實可省,但寫出來意圖更清楚。 - 選測資的理由(好測資要覆蓋所有分支):
- 2024:走
A && B為真的路徑; - 1900:A真、B假、C假 → 整體假(世紀年陷阱,正是規則存在的原因); - 2000:A && B假(被 100 整除)但C真 → 整體真; - 2025:A假,靠短路直接判定假。 - 輸出設計:邏輯運算式的值就是
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)「先除再轉」的錯誤示範,
計算平均並印出,觀察並解釋差異。
解題思路
- 手算基準:88+73+95 = 256,256 ÷ 3 = 85.333…。
- 整數版錯在哪:
sum / 3兩邊都是 int → 整數除法 → 85,小數沒了。 - 兩種正解,原理相同——讓除法「至少一邊」是浮點:
-
(double)sum / 3:cast 的優先順序高於/,sum先變 256.0; -sum / 3.0:字面值本身就是 double。 - 關鍵陷阱:
(double)(sum / 3)——括號讓整數除法先發生, 85 再轉成 85.0,為時已晚。cast 的位置決定一切。 - 驗證:四個版本一起印出對照,差異一目瞭然。
參考解答
#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)。
解題思路
?:的定位:它是「會回傳值的 if/else」。當你要的是二選一的值 (而不是二選一的動作)時,?:讓程式更精簡。- (a) 取大值:
(a > b) ? a : b——條件真取 a、假取 b。這是?:的教科書用法。 - (b) 奇偶:判斷
a % 2 != 0,然後選的不是數字而是字串"odd"/"even", 直接塞進printf的%s。?:選什麼型別的值都行。 - (c) 三數取最大:分治思考——先比 a、b,「勝者」再與 c 比:
(a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c)。 寫的時候用換行與縮排對齊兩個分支;讀的時候由外往內拆。 誠實的結論:兩層已是可讀性極限,三層以上請改if/else或拆函式。 - 單複數技巧:
(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:優先順序偵探(難度:進階)
題目
先不執行程式,手算下面三條運算式的值,再寫程式驗證:
2 + 3 * 4 % 510 > 9 > 8(特別解釋:為什麼結果不是「數學上」的真?)1 << 2 + 1(提示:+的優先順序比<<高)
解題思路
- 案例一:
*與%同級且高於+,同級由左到右:3 * 4= 12 →12 % 5= 2 →2 + 2= 4。 - 案例二:C 沒有數學的連鎖比較!關係運算子由左到右結合:
10 > 9先算出 1(真),接著1 > 8為假 → 整條式子是 0。 想表達「9 介於 8 和 10 之間」必須寫(10 > 9) && (9 > 8)。 注意:新版 Apple clang 直接把10 > 9 > 8這種寫法當成錯誤擋下 (chained comparison),逼你把意圖寫清楚——這是編譯器在救你。 - 案例三:出乎多數人意料,
+高於<<:1 << 2 + 1是1 << (2 + 1)=1 << 3= 8,不是(1 << 2) + 1= 5。 移位運算的優先順序是歷史包袱,永遠加括號。 clang 對此也有警告(-Wshift-op-parentheses),所以參考解答直接寫出加括號的兩種語意。 - 方法論:手算 → 預測 → 執行對答案。答案不同時,回頭查優先順序表—— 這個「預測再驗證」的迴路是把優先順序表內化的最快方法。
參考解答
#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;差別只在運算式本身的值——
後置
i++的值是「舊值」,前置++i的值是「新值」。 - (a) 單獨敘述:運算式的值沒人使用,所以兩者完全等價。 結論:單獨用時選哪個都對,全專案風格一致即可。
- (b) 指定右邊:值被接住了,差異現形——
post = x++→ post 拿舊值 5,x 變 6;pre = ++y→ y 先變 6,pre 拿 6。 - (c) 函式引數:規則同 (b)。設計一個
show(int value)函式把收到的值印出來, 就能「觀測」傳進去的是舊值還是新值:show(m++)收到 10、show(++n)收到 11。 - 未定義行為的邊界:一對定序點之間,同一變數「改了又讀」或「改兩次」就是 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 = 6vspre = 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++; 一行完成 |