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

解題思路

  1. 為資料選型別——這是本題真正要練的事: - 年齡是整數 → int; - 身高有小數 → double(浮點預設選 double,float 精度常不夠); - 血型是單一字元 → char,字元常數用單引號 'O'。
  2. 宣告時就初始化:int age = 20; 一氣呵成,杜絕「垃圾值」問題。
  3. 一個 printf 帶三個值:格式字串挖三個洞,型別要一一對應—— %d ← int、%f ← double、%c ← char。
  4. 修飾輸出:%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。

解題思路

  1. 先手算正確答案:37.5 × 1.8 + 32 = 67.5 + 32 = 99.5。有了基準才能談「差異」。
  2. 整數版會壞在哪(預測): - 37.5 根本塞不進 int——一存進去就截斷成 37(資訊在第一步就丟了); - 37 * 9 / 5 = 333 / 5 = 66(整數除法再丟一次小數);66 + 32 = 98,比正解少 1.5。
  3. 浮點版:全部用 double 與浮點字面值(9.0、5.0),一路都是浮點運算 → 99.5。
  4. 陷阱版:c_dbl * (9 / 5) + 32——9 / 5 兩邊都是 int,先算成 1, 整條公式變成 37.5 + 32 = 69.5。教訓:混合運算式中,每個子運算式的型別要逐一檢查。
  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 交叉驗證。

解題思路

  1. sizeof 怎麼印:sizeof 的結果型別是 size_t(無號),對應的格式是 %zu。 用 %d 印會被 -Wformat 警告——這正是本題想讓你踩一次的點。
  2. 驗證邏輯:知道大小之後可以「反推」極限值—— int 是 4 bytes = 32 bits,有號數拿 1 位當符號,最大值應是 2³¹ − 1 = 2147483647。 印出 INT_MAX 對照,數字吻合就證明理解正確。
  3. 格式對應:INT_MAX 用 %d、LONG_MAX 是 long 要用 %ld、 LLONG_MAX 要用 %lld。格式與型別的配對是本單元的核心紀律。
  4. 對齊小技巧:表頭與資料用固定寬度排版,輸出可讀性大增(更完整的寬度控制見單元 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:進位法(難度:基礎)

題目

  1. 宣告 int x = 255;,用 %d、%o、%x(以及 %X)分別印出十進位、八進位、十六進位。
  2. 反過來:在原始碼中用十六進位寫 0x1F、用八進位寫 037,各用 %d 印出十進位值, 並驗證兩者是同一個值。

解題思路

  1. 核心觀念:變數裡存的是「值」(一串位元),十進位/八進位/十六進位只是印出來的樣子。 255 這個值可以印成 255、377(八進位)、ff(十六進位),值始終只有一個。
  2. 手算驗證:255 = 3×64 + 7×8 + 7 → 八進位 377;255 = 15×16 + 15 → 十六進位 FF。
  3. 反向部分:0x1F = 1×16 + 15 = 31;037 = 3×8 + 7 = 31。兩者相等。 用 h == o 印出 1(真)做程式化驗證。
  4. 展示格式字串本身:想在輸出裡印出 %d 這三個字元,要寫 %%d——%% 印一個 %。
  5. 警惕: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' 轉成大寫。

解題思路

  1. 字元常數的本質:'A' 在 C 中的型別其實是 int,值是它的 ASCII 編碼 65。 所以字元可以直接做算術:'A' + 1 就是 66。
  2. 同一個值、兩種印法:66 用 %d 印是 66,用 %c 印是 B—— 再次印證「值只有一個,格式決定呈現」。
  3. 推導大小寫轉換:查 ASCII 表——'A'~'Z' 是 65~90、'a'~'z' 是 97~122。 兩段各自連續,且每對大小寫的差固定是 97 − 65 = 32 = 'a' - 'A'。 所以:大寫 + 32 = 小寫;小寫 − 32 = 大寫。
  4. 為什麼寫 ('a' - 'A') 而不是 32:程式碼自我說明(一看就知道是大小寫差值), 而且不必背數字——讓編譯器替你算。
  5. 邊界提醒:這個轉換只對「已知是字母」的字元正確;亂套在數字或標點上會得到怪字元。 通用的判斷寫法在單元 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:溢位觀察(難度:挑戰)

題目

  1. 宣告 unsigned char uc = 255;,執行 uc = uc + 1; 印出結果並解釋;再 uc = uc - 1; 觀察反向環繞。
  2. 宣告 unsigned int u = UINT_MAX;,印出 u + 1 的值並解釋為什麼這是「明確定義」的行為。
  3. 印出 INT_MAX,用文字說明(程式中不要真的執行)為什麼 int 的 INT_MAX + 1 是未定義行為,以及「未定義」在實務上意味著什麼。

解題思路

  1. 無號整數的環繞是規格保證的:C 標準規定無號運算「對 2ⁿ 取模」。 unsigned char 是 8 位元(n = 8):255 + 1 = 256,256 mod 256 = 0; 0 − 1 則繞回 255。這不是 bug,是可依賴的行為(雜湊、計數器常刻意利用)。
  2. 中間有個細節:uc + 1 運算時 uc 先升為 int(值 256 不溢位), 是「存回 unsigned char」的那一步做了 mod 256。結果一樣,但理解順序才能舉一反三。
  3. unsigned int 版:UINT_MAX(4294967295)+ 1 → mod 2³² → 0。同一條規則、不同位寬。
  4. 有號溢位為什麼不同:標準對 INT_MAX + 1 不定義任何結果。 編譯器最佳化時直接假設有號溢位不會發生,例如把 x + 1 > x 直接優化成常數真。 所以「我試過會繞回負數」不可靠——換個編譯器、開個 -O2 行為就變。 正確做法:運算前檢查(如 if (x > INT_MAX - y)),這在中階單元展開。
  5. 示範的分寸:程式印出 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 繞回負數 未定義行為:最佳化後行為不可預測 運算前先檢查範圍