本檔提供本單元練習題的完整詳解。先自己動手做:每題先只看「題目」,
寫完、編譯、執行後再對照「解題思路」與「參考解答」。
完整程式碼都在 exercises/ 資料夾,可直接編譯:
cc -std=c11 -Wall -Wextra -o /tmp/out exercises/ex1_profile.c && /tmp/out
練習 1:個人資料變數(難度:基礎)
題目
宣告三個變數:年齡(int)、身高公尺(double)、血型(char),
全部在宣告時就初始化,然後用一個 printf 印成一句話。
輸出範例:
I am 20 years old, 1.72 meters tall, and my blood type is O.
解題思路
- 為資料選型別——這是本題真正要練的事:
- 年齡是整數 →
int; - 身高有小數 →double(浮點預設選 double,float 精度常不夠); - 血型是單一字元 →char,字元常數用單引號'O'。 - 宣告時就初始化:
int age = 20;一氣呵成,杜絕「垃圾值」問題。 - 一個 printf 帶三個值:格式字串挖三個洞,型別要一一對應——
%d← int、%f← double、%c← char。 - 修飾輸出:
%f預設印 6 位小數(1.720000),身高用%.2f取兩位更自然。
參考解答
#include <stdio.h>
int main(void)
{
int age = 20; /* 年齡:整數,用 int */
double height_m = 1.72; /* 身高(公尺):有小數,用 double */
char blood_type = 'O'; /* 血型:單一字元,用 char(單引號!) */
/* 一個 printf、三個洞:%d 對 int、%.2f 對 double(兩位小數)、%c 對 char */
printf("I am %d years old, %.2f meters tall, and my blood type is %c.\n",
age, height_m, blood_type);
return 0;
}
(完整檔案:exercises/ex1_profile.c)
程式碼講解
'O'(單引號)是字元常數;"O"(雙引號)是字串——兩者型別完全不同,%c只能配字元。混用是初學前三名的錯誤。%.2f的.2是「精度」:小數點後兩位。精度控制的細節在單元 09 完整展開。- printf 的引數順序必須與洞的順序一致:
age, height_m, blood_type對應%d, %.2f, %c。
範例輸出
I am 20 years old, 1.72 meters tall, and my blood type is O.
練習 2:溫度轉換——整數 vs 浮點(難度:基礎)
題目
把攝氏 37.5 度轉成華氏,公式 F = C × 9 / 5 + 32。
分別用「整數運算」與「浮點運算」各算一次並印出,觀察差異;
另外示範一個常見陷阱:變數都是 double,公式裡卻寫了 9 / 5。
解題思路
- 先手算正確答案:37.5 × 1.8 + 32 = 67.5 + 32 = 99.5。有了基準才能談「差異」。
- 整數版會壞在哪(預測):
- 37.5 根本塞不進
int——一存進去就截斷成 37(資訊在第一步就丟了); -37 * 9 / 5= 333 / 5 = 66(整數除法再丟一次小數);66 + 32 = 98,比正解少 1.5。 - 浮點版:全部用
double與浮點字面值(9.0、5.0),一路都是浮點運算 → 99.5。 - 陷阱版:
c_dbl * (9 / 5) + 32——9 / 5兩邊都是 int,先算成 1, 整條公式變成37.5 + 32 = 69.5。教訓:混合運算式中,每個子運算式的型別要逐一檢查。 - 轉型注意:把
37.5存進int時,寫明(int)37.5—— 讓「我知道會截斷」的意圖清楚可見,也避免編譯器的隱式轉換警告。
參考解答
#include <stdio.h>
int main(void)
{
/* --- 整數版:37.5 存進 int 就已經先毀了 --- */
int c_int = (int)37.5; /* 截斷成 37:小數部分直接丟掉 */
int f_int = c_int * 9 / 5 + 32; /* 37*9=333, 333/5=66(整數除法), +32=98 */
printf("integer version: C = %d -> F = %d\n", c_int, f_int);
/* --- 浮點版:全程 double,才是正確答案 --- */
double c_dbl = 37.5;
double f_dbl = c_dbl * 9.0 / 5.0 + 32.0; /* 37.5*1.8+32 = 99.5 */
printf("floating version: C = %.1f -> F = %.1f\n", c_dbl, f_dbl);
/* --- 陷阱版:變數是 double,公式裡卻藏了整數除法 9/5 --- */
double f_trap = c_dbl * (9 / 5) + 32; /* 9/5 先算成 1! */
printf("trap version: C = %.1f -> F = %.1f (9/5 became 1)\n",
c_dbl, f_trap);
return 0;
}
(完整檔案:exercises/ex2_temperature.c)
程式碼講解
c_int * 9 / 5:乘除同級、由左到右——先乘再除,333 / 5才截斷成 66。 如果寫成c_int * (9 / 5)會更慘(先變成* 1)。先乘後除是整數公式的護身符。- 浮點版把常數寫成
9.0、5.0:只要除法「任一邊」是浮點,整個除法就是浮點除法。 - 陷阱版是實務中最常見的 bug 形態:公式看起來對、變數型別也對, 壞在「常數對常數」的子運算式。
範例輸出
integer version: C = 37 -> F = 98
floating version: C = 37.5 -> F = 99.5
trap version: C = 37.5 -> F = 69.5 (9/5 became 1)
練習 3:型別偵查(難度:基礎)
題目
不看單元 README 的表格,寫程式印出你機器上 char、short、int、long、
long long、float、double 的 sizeof;再印出 <limits.h> 的
CHAR_BIT、INT_MAX、INT_MIN、LONG_MAX、LLONG_MAX 交叉驗證。
解題思路
- sizeof 怎麼印:
sizeof的結果型別是size_t(無號),對應的格式是%zu。 用%d印會被-Wformat警告——這正是本題想讓你踩一次的點。 - 驗證邏輯:知道大小之後可以「反推」極限值——
int是 4 bytes = 32 bits,有號數拿 1 位當符號,最大值應是 2³¹ − 1 = 2147483647。 印出INT_MAX對照,數字吻合就證明理解正確。 - 格式對應:
INT_MAX用%d、LONG_MAX是 long 要用%ld、LLONG_MAX要用%lld。格式與型別的配對是本單元的核心紀律。 - 對齊小技巧:表頭與資料用固定寬度排版,輸出可讀性大增(更完整的寬度控制見單元 09)。
參考解答
#include <stdio.h>
#include <limits.h>
int main(void)
{
/* sizeof 的結果型別是 size_t,printf 要用 %zu */
printf("type size (bytes)\n");
printf("---------- ------------\n");
printf("char %zu\n", sizeof(char));
printf("short %zu\n", sizeof(short));
printf("int %zu\n", sizeof(int));
printf("long %zu\n", sizeof(long));
printf("long long %zu\n", sizeof(long long));
printf("float %zu\n", sizeof(float));
printf("double %zu\n", sizeof(double));
/* 用極限值反推位元數:int 4 bytes = 32 bits,
* 有號的最大值應該是 2^31 - 1 = 2147483647 */
printf("\ncross-check with <limits.h>:\n");
printf("CHAR_BIT = %d (bits per byte)\n", CHAR_BIT);
printf("INT_MAX = %d\n", INT_MAX);
printf("INT_MIN = %d\n", INT_MIN);
printf("LONG_MAX = %ld\n", LONG_MAX);
printf("LLONG_MAX = %lld\n", LLONG_MAX);
return 0;
}
(完整檔案:exercises/ex3_sizeof.c)
程式碼講解
sizeof(char)依標準保證是 1;其他型別的大小依平台而定,所以才要「用程式問機器」。- macOS(LP64 模型)上
long與long long都是 8 bytes,所以兩個 MAX 印出同樣的數字—— 這不是 bug,是平台特性;Windows(LLP64)上long只有 4 bytes。 INT_MIN是 −2147483648:負數範圍比正數多一個(二補數表示法的特性)。
範例輸出
type size (bytes)
---------- ------------
char 1
short 2
int 4
long 8
long long 8
float 4
double 8
cross-check with <limits.h>:
CHAR_BIT = 8 (bits per byte)
INT_MAX = 2147483647
INT_MIN = -2147483648
LONG_MAX = 9223372036854775807
LLONG_MAX = 9223372036854775807
練習 4:進位法(難度:基礎)
題目
- 宣告
int x = 255;,用%d、%o、%x(以及%X)分別印出十進位、八進位、十六進位。 - 反過來:在原始碼中用十六進位寫
0x1F、用八進位寫037,各用%d印出十進位值, 並驗證兩者是同一個值。
解題思路
- 核心觀念:變數裡存的是「值」(一串位元),十進位/八進位/十六進位只是印出來的樣子。
255 這個值可以印成
255、377(八進位)、ff(十六進位),值始終只有一個。 - 手算驗證:255 = 3×64 + 7×8 + 7 → 八進位 377;255 = 15×16 + 15 → 十六進位 FF。
- 反向部分:
0x1F= 1×16 + 15 = 31;037= 3×8 + 7 = 31。兩者相等。 用h == o印出 1(真)做程式化驗證。 - 展示格式字串本身:想在輸出裡印出
%d這三個字元,要寫%%d——%%印一個%。 - 警惕:
037開頭的0不是裝飾——0 開頭的整數字面值就是八進位。 寫int day = 08;會直接編譯錯誤(8 不是八進位數字),這是日期/編號補零時的經典地雷。
參考解答
#include <stdio.h>
int main(void)
{
int x = 255;
/* 同一個值、三種呈現方式——值只有一個,進位法只是「印法」 */
printf("x = 255:\n");
printf(" decimal (%%d): %d\n", x);
printf(" octal (%%o): %o\n", x);
printf(" hex (%%x): %x\n", x);
printf(" hex (%%X): %X\n", x);
/* 反向:原始碼裡用八進位/十六進位「寫」,用十進位「印」 */
int h = 0x1F; /* 0x 開頭:十六進位 1F = 1*16 + 15 = 31 */
int o = 037; /* 0 開頭:八進位 37 = 3*8 + 7 = 31 */
printf("\nliterals written in other bases:\n");
printf(" 0x1F as decimal: %d\n", h);
printf(" 037 as decimal: %d\n", o);
printf(" (both are the same value: %d)\n", h == o);
return 0;
}
(完整檔案:exercises/ex4_bases.c)
程式碼講解
%o、%x、%X只是「翻譯輸出」,不改變變數內容;%x印小寫ff、%X印大寫FF。h == o是關係運算式,結果是int的 1(真)或 0(假)——直接用%d印。- 想在十六進位輸出前面加
0x前綴,可用%#x(單元 09 的旗標)。
範例輸出
x = 255:
decimal (%d): 255
octal (%o): 377
hex (%x): ff
hex (%X): FF
literals written in other bases:
0x1F as decimal: 31
037 as decimal: 31
(both are the same value: 1)
練習 5:字元即整數(難度:進階)
題目
印出 'A'、'A' + 1、'a' - 'A' 的整數值,並回答:
為什麼 'a' - 'A' 可以用來做大小寫轉換?寫程式示範把 'G' 轉成小寫、'q' 轉成大寫。
解題思路
- 字元常數的本質:
'A'在 C 中的型別其實是int,值是它的 ASCII 編碼 65。 所以字元可以直接做算術:'A' + 1就是 66。 - 同一個值、兩種印法:66 用
%d印是66,用%c印是B—— 再次印證「值只有一個,格式決定呈現」。 - 推導大小寫轉換:查 ASCII 表——
'A'~'Z'是 65~90、'a'~'z'是 97~122。 兩段各自連續,且每對大小寫的差固定是 97 − 65 = 32 ='a' - 'A'。 所以:大寫 + 32 = 小寫;小寫 − 32 = 大寫。 - 為什麼寫
('a' - 'A')而不是 32:程式碼自我說明(一看就知道是大小寫差值), 而且不必背數字——讓編譯器替你算。 - 邊界提醒:這個轉換只對「已知是字母」的字元正確;亂套在數字或標點上會得到怪字元。 通用的判斷寫法在單元 08 的字元分類會練到。
參考解答
#include <stdio.h>
int main(void)
{
/* 字元常數的本質是 int:'A' 就是 65 */
printf("'A' as int = %d\n", 'A');
printf("'A' + 1 as int = %d\n", 'A' + 1);
printf("'A' + 1 as char = %c\n", 'A' + 1); /* 65+1=66 → 'B' */
printf("'a' - 'A' = %d\n", 'a' - 'A'); /* 97-65 = 32 */
/* 大小寫轉換的原理:ASCII 中大小寫字母各自連續、
* 且每對大小寫差固定 32(= 'a' - 'A')。 */
char upper = 'G';
char lower = upper + ('a' - 'A'); /* 大寫 → 小寫:加上差值 */
printf("\n'%c' + ('a' - 'A') = '%c'\n", upper, lower);
char c2 = 'q';
char up2 = c2 - ('a' - 'A'); /* 小寫 → 大寫:減去差值 */
printf("'%c' - ('a' - 'A') = '%c'\n", c2, up2);
return 0;
}
(完整檔案:exercises/ex5_char_int.c)
程式碼講解
printf("%d", 'A')完全合法——'A'本來就是 int,不需要任何轉換。upper + ('a' - 'A'):char參與運算會先升為int(integer promotion), 算完 103 再存回char,用%c印出g。- 這個「字元差值」技巧同樣是
c - '0'(數字字元轉數值)的原理,單元 07、08 會大量使用。
範例輸出
'A' as int = 65
'A' + 1 as int = 66
'A' + 1 as char = B
'a' - 'A' = 32
'G' + ('a' - 'A') = 'g'
'q' - ('a' - 'A') = 'Q'
練習 6:溢位觀察(難度:挑戰)
題目
- 宣告
unsigned char uc = 255;,執行uc = uc + 1;印出結果並解釋;再uc = uc - 1;觀察反向環繞。 - 宣告
unsigned int u = UINT_MAX;,印出u + 1的值並解釋為什麼這是「明確定義」的行為。 - 印出
INT_MAX,用文字說明(程式中不要真的執行)為什麼int的INT_MAX + 1是未定義行為,以及「未定義」在實務上意味著什麼。
解題思路
- 無號整數的環繞是規格保證的:C 標準規定無號運算「對 2ⁿ 取模」。
unsigned char是 8 位元(n = 8):255 + 1 = 256,256 mod 256 = 0; 0 − 1 則繞回 255。這不是 bug,是可依賴的行為(雜湊、計數器常刻意利用)。 - 中間有個細節:
uc + 1運算時uc先升為int(值 256 不溢位), 是「存回unsigned char」的那一步做了 mod 256。結果一樣,但理解順序才能舉一反三。 - unsigned int 版:UINT_MAX(4294967295)+ 1 → mod 2³² → 0。同一條規則、不同位寬。
- 有號溢位為什麼不同:標準對
INT_MAX + 1不定義任何結果。 編譯器最佳化時直接假設有號溢位不會發生,例如把x + 1 > x直接優化成常數真。 所以「我試過會繞回負數」不可靠——換個編譯器、開個-O2行為就變。 正確做法:運算前檢查(如if (x > INT_MAX - y)),這在中階單元展開。 - 示範的分寸:程式印出
INT_MAX與說明文字即可,不要真的執行有號溢位—— 示範未定義行為本身就是錯誤示範。
參考解答
#include <stdio.h>
#include <limits.h>
int main(void)
{
/* --- 無號 char 的環繞 --- */
unsigned char uc = 255; /* unsigned char 的最大值(8 位元) */
printf("uc = %d\n", uc);
uc = uc + 1; /* 256 塞不進 8 位元 → 對 256 取模 → 0 */
printf("uc + 1 = %d (wrapped around: 256 mod 256 = 0)\n", uc);
uc = uc - 1; /* 0 - 1 → 環繞到最大值 255 */
printf("uc - 1 = %d (wrapped the other way)\n", uc);
/* --- 無號 int 的環繞(同樣是明確定義的) --- */
unsigned int u = UINT_MAX;
printf("\nUINT_MAX = %u\n", u);
printf("UINT_MAX + 1 = %u (defined: modulo 2^32)\n", u + 1u);
/* --- 有號 int:只展示極限值,不真的溢位 --- */
printf("\nINT_MAX = %d\n", INT_MAX);
printf("INT_MAX + 1 would be SIGNED overflow: undefined behavior.\n");
printf("The compiler may assume it never happens -- do not rely on it.\n");
return 0;
}
(完整檔案:exercises/ex6_overflow.c)
程式碼講解
unsigned char用%d印是安全的:它先升為int(0~255 一定裝得下)。u + 1u:後綴u讓1是 unsigned,整條運算式都是無號運算;%u是無號 int 的格式。- 三段輸出對應題目三個要求:8 位元環繞、32 位元環繞、有號溢位的「只說不做」。
- 記住結論的最簡形式:無號溢位 = 環繞(defined);有號溢位 = 未定義(UB)。
範例輸出
uc = 255
uc + 1 = 0 (wrapped around: 256 mod 256 = 0)
uc - 1 = 255 (wrapped the other way)
UINT_MAX = 4294967295
UINT_MAX + 1 = 0 (defined: modulo 2^32)
INT_MAX = 2147483647
INT_MAX + 1 would be SIGNED overflow: undefined behavior.
The compiler may assume it never happens -- do not rely on it.
常見錯誤解法對照
| 錯誤寫法 | 症狀 | 正解 |
|---|---|---|
char blood = "O"; |
用字串初始化 char,編譯警告/錯誤 | char blood = 'O';(單引號) |
int c = 37.5; 無轉型 |
clang 警告 implicit conversion changes value | (int)37.5 明示截斷意圖 |
printf("%d", sizeof(int)) |
-Wformat 警告:size_t 不是 int |
用 %zu |
double f = c * (9 / 5) + 32; |
9/5 先算成 1,結果差一大截 | 9.0 / 5.0 或先乘後除 |
printf("%d", LONG_MAX) |
格式與 long 不符:未定義行為 | %ld |
依賴 INT_MAX + 1 繞回負數 |
未定義行為:最佳化後行為不可預測 | 運算前先檢查範圍 |