本檔收錄本單元的練習題、完整解題思路與參考解答。 強烈建議先自己動手做:看完題目就闔上這份檔案,寫出你的版本、編譯、執行、卡住了再回來看「解題思路」; 真的做完了,再對照「參考解答」與「程式碼講解」。每題的完整程式碼也放在 exercises/ 資料夾,可直接編譯:

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

練習 1:ABS 巨集 —— 括號守則(難度:基礎)

題目

寫函式式巨集 #define ABS(x) 計算絕對值,要求以下三個測資全部正確:

ABS(-5)        /* 5 */
ABS(3 - 7)     /* 4 */
10 - ABS(-2)   /* 8 */

並寫一個不加括號的壞版本,示範 10 - BAD_ABS(-2) 為什麼算錯。

解題思路

  1. 巨集只是文字取代——前置處理器不懂優先權、不懂型別, 它只是把 ABS(某文字) 換成定義裡的樣子。所以安全守則是機械性的: 每個參數出現處都加括號,整個本體再加一層括號。
#define ABS(x) ((x) < 0 ? -(x) : (x))
  1. 壞版本壞在哪? #define BAD_ABS(x) x < 0 ? -x : x, 代入 10 - BAD_ABS(-2) 展開成:
10 - -2 < 0 ? - -2 : -2

依優先權:算術 - 最先、< 其次、?: 最後,所以它其實是:

((10 + 2) < 0) ? 2 : -2   ->  條件為假  ->  -2

期望 8、得到 -2——沒有任何編譯警告,安靜地算錯。 3. 測資的設計:ABS(3-7) 驗參數括號(沒括號時 - 混入外界運算)、 10 - ABS(-2) 驗本體外層括號(?: 優先權最低,沒括號會被外面的 - 拆散)。 自己出巨集題時,永遠要有「引數是運算式」與「巨集嵌在運算式中」兩類測資。

參考解答

/*
 * ex1_abs.c
 * ---------
 * 練習 1:ABS 巨集 —— 括號守則(難度:基礎)
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex1_abs.c
 * 執行:/tmp/out
 */
#include <stdio.h>

/* 正確版:每個參數加括號、整個本體加括號 */
#define ABS(x) ((x) < 0 ? -(x) : (x))

/* 壞掉版:完全不加括號(示範用,別學!) */
#define BAD_ABS(x) x < 0 ? -x : x

int main(void)
{
    /* 三個驗收測資 */
    printf("ABS(-5)      = %d\n", ABS(-5));        /* 5 */
    printf("ABS(3-7)     = %d\n", ABS(3 - 7));     /* 4 */
    printf("10 - ABS(-2) = %d\n", 10 - ABS(-2));   /* 8 */

    /*
     * 壞掉版的災難:10 - BAD_ABS(-2) 展開成
     *   10 - -2 < 0 ? -(-2) : -2
     * 因為 - 的優先權高於 <、< 高於 ?:,實際上是
     *   ((10 + 2) < 0) ? 2 : -2  ->  -2(應為 8!)
     */
    printf("10 - BAD_ABS(-2) = %d(應為 8,卻是 -2!)\n",
           10 - BAD_ABS(-2));

    return 0;
}

程式碼講解

  • ((x) < 0 ? -(x) : (x)):三個 (x) 一個都不能省—— 想像 x 被換成 3 - 7 這種帶運算子的文字,任何一處裸露都可能被鄰居搶走。
  • 即使全加了括號,ABS(i++) 仍然是災難:x 出現三次, 副作用最多重複三次。括號守則救不了副作用——那是巨集的另一條天生限制。
  • ABS 還有一個微妙陷阱:ABS(INT_MIN) 中 -(INT_MIN) 溢位(未定義行為)。 標準庫的 abs() 也有同樣問題——這不是巨集的錯,是二補數的不對稱。

範例輸出

ABS(-5)      = 5
ABS(3-7)     = 4
10 - ABS(-2) = 8
10 - BAD_ABS(-2) = -2(應為 8,卻是 -2!)

練習 2:ARRAY_LEN 與陣列退化(難度:基礎)

題目

寫巨集 #define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])),用它走訪一個陣列; 再示範並解釋:為什麼它對「函式參數的陣列」會失效?

解題思路

  1. 原理:陣列的總位元組數除以單一元素的位元組數 = 元素個數。 sizeof 是編譯期運算子,整個巨集在編譯期就變成常數,零執行成本。 (a)[0] 的寫法讓元素型別改變時巨集自動跟上——不用寫死 sizeof(int)。
  2. 它失效的唯一場景,也是最容易踩的:陣列一旦傳進函式就退化(decay) 成指向首元素的指標。函式簽名寫 int a[] 或 int *a 完全等價—— 函式裡的 sizeof(a) 是指標的大小(macOS 上 8),不是陣列的大小:
main 裡:  scores 是 int[7]   sizeof = 28   ARRAY_LEN = 28/4 = 7 ✓
函式裡:  a 是 const int *    sizeof =  8   「長度」 =  8/4 = 2 ✗(永遠是 2)
  1. 編譯器其實認得這個經典錯誤:直接在函式裡寫 ARRAY_LEN(a), clang 會發出 -Wsizeof-pointer-div 警告。解答中拆成兩步計算 (先取 sizeof a、再相除)讓錯誤的來源看得一清二楚。
  2. 結論與正確做法:元素個數必須在定義陣列的作用域(陣列還是陣列的地方) 用 ARRAY_LEN 算好,以參數傳給函式——這正是本教程所有 f(arr, n) 簽名的由來。

參考解答

/*
 * ex2_array_len.c
 * ---------------
 * 練習 2:ARRAY_LEN 巨集與它對函式參數失效的原因(難度:基礎)
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex2_array_len.c
 * 執行:/tmp/out
 */
#include <stdio.h>

/*
 * 元素個數 = 整個陣列的位元組數 / 一個元素的位元組數。
 * 只對「真正的陣列」有效——對指標會得到錯誤的答案(見下)。
 */
#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))

/*
 * 陣列傳進函式後「退化」成指標:
 * 這裡的 a 其實是 const int *,sizeof(a) 是指標大小(8),不是陣列大小!
 *
 * 若直接寫 return ARRAY_LEN(a);,clang 會發出
 * -Wsizeof-pointer-div 警告(它認得這個經典錯誤)——
 * 這裡拆成兩步計算,讓你看清楚錯誤答案是怎麼來的。
 */
size_t broken_len(const int *a)
{
    size_t size_of_param = sizeof a;      /* 指標的大小:8 */
    size_t size_of_elem = sizeof a[0];    /* 一個 int:4 */
    return size_of_param / size_of_elem;  /* 永遠是 2,跟陣列長度無關 */
}

int main(void)
{
    int scores[] = { 90, 75, 88, 62, 100, 45, 71 };

    /* 在陣列還是陣列的地方(定義它的作用域)使用:正確 */
    printf("ARRAY_LEN(scores) = %zu\n", ARRAY_LEN(scores));

    printf("走訪:");
    for (size_t i = 0; i < ARRAY_LEN(scores); i++)
        printf("%d ", scores[i]);
    printf("\n\n");

    /* 傳進函式後陣列退化成指標:巨集失效 */
    printf("broken_len(scores) = %zu(應為 7,卻是 %zu!)\n",
           broken_len(scores), broken_len(scores));
    printf("原因:函式參數的『陣列』其實是指標,\n");
    printf("      sizeof(指標)/sizeof(int) = %zu/%zu = %zu\n",
           sizeof(int *), sizeof(int), sizeof(int *) / sizeof(int));
    printf("結論:元素個數要在定義陣列的作用域算好,當參數傳進去。\n");

    return 0;
}

程式碼講解

  • ARRAY_LEN(scores) 在 main 裡回 7:scores 在它的定義作用域內 是貨真價實的 int[7],sizeof 得到 28。
  • 迴圈變數用 size_t i:sizeof 的結果型別是 size_t(無號), 與 int i 比較會觸發 -Wextra 的 signed/unsigned 比較警告。
  • broken_len 的答案「永遠是 2」——與陣列實際多長完全無關, 只由 sizeof(int*)/sizeof(int) = 8/4 決定。這種「看起來會動、 答案永遠錯」的函式比直接編譯錯誤更危險。
  • 加分知識:C99 的 static 陣列參數(int a[static 7])與 C++ 的參考陣列都是為了補救退化——C 的主流答案仍是「多傳一個 n」。

範例輸出

ARRAY_LEN(scores) = 7
走訪:90 75 88 62 100 45 71 

broken_len(scores) = 2(應為 7,卻是 2!)
原因:函式參數的『陣列』其實是指標,
      sizeof(指標)/sizeof(int) = 8/4 = 2
結論:元素個數要在定義陣列的作用域算好,當參數傳進去。

練習 3:CLAMP 巨集(難度:基礎)

題目

寫巨集 #define CLAMP(x, lo, hi):把 x 夾在 [lo, hi] 區間內 (小於 lo 回 lo、大於 hi 回 hi、否則回 x)。 測試整數(成績夾在 0..100)與浮點數(音量夾在 0.0..1.0), 並驗證引數是運算式時仍正確。

解題思路

  1. 邏輯只有三分支,巢狀條件運算子一行寫完:
x < lo  ->  lo
x > hi  ->  hi
其他    ->  x
#define CLAMP(x, lo, hi) ((x) < (lo) ? (lo) : ((x) > (hi) ? (hi) : (x)))
  1. 括號:?: 是右結合,內層 ?: 不加括號其實也會正確配對, 但明確括起來可讀性與安全性都更好;三個參數的每次出現照例全部括號。
  2. 為什麼用巨集而不是函式? 這題正好是「巨集泛型」的展示場: 同一個 CLAMP 直接用在 int 和 double,函式版本得寫兩個 (clamp_i、clamp_d)。代價照舊:x 在本體出現三次, 不能傳 CLAMP(x++, 0, 100)。
  3. 測資:邊界值(恰好 = lo、恰好 = hi)、區間外兩側、 運算式引數(a - b * 2)——夾取函式的 off-by-one 最愛藏在邊界。

參考解答

/*
 * ex3_clamp.c
 * -----------
 * 練習 3:CLAMP 巨集 —— 巢狀條件運算子與括號(難度:基礎)
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex3_clamp.c
 * 執行:/tmp/out
 */
#include <stdio.h>

/*
 * 把 x 夾在 [lo, hi] 區間內:
 *   x < lo -> lo;x > hi -> hi;否則 -> x
 * ?: 是右結合,巢狀寫法會正確配對,但仍然每個參數、整個本體都加括號。
 */
#define CLAMP(x, lo, hi) ((x) < (lo) ? (lo) : ((x) > (hi) ? (hi) : (x)))

int main(void)
{
    /* 整數:把成績夾在 0..100 */
    int raw[] = { -20, 0, 55, 100, 137 };
    printf("夾在 [0, 100]:\n");
    for (int i = 0; i < 5; i++)
        printf("  CLAMP(%4d) = %d\n", raw[i], CLAMP(raw[i], 0, 100));

    /* 巨集的泛型效果:同一個巨集直接用在 double */
    double vol = 1.75;
    printf("\n音量 %.2f 夾在 [0.0, 1.0] = %.2f\n",
           vol, CLAMP(vol, 0.0, 1.0));

    /* 括號守則的驗證:引數是運算式也正確 */
    int a = 7, b = 3;
    printf("CLAMP(a - b * 2, 2, 6) = %d(a-b*2 = 1,夾成 2)\n",
           CLAMP(a - b * 2, 2, 6));

    return 0;
}

程式碼講解

  • 邊界測資 0 與 100 都原樣通過(< 與 > 是嚴格不等), [lo, hi] 是閉區間——若誤寫 <=/>= 行為其實相同, 但邏輯上「等於邊界要不要動」在別的題目(如迴圈範圍)就有差了。
  • CLAMP(vol, 0.0, 1.0) 直接對 double 工作——泛型是巨集少數勝過函式的地方 (C11 之後也可用 _Generic 讓函式家族有泛型門面)。
  • 隱藏陷阱:混用型別如 CLAMP(x, 0, 100.5) 時,?: 的型別提升規則 會決定結果型別——巨集不會幫你檢查,這是「無型別檢查」代價的實例。
  • x 出現三次 → 最多求值兩次(條件一次+分支一次), CLAMP(f(), 0, 100) 可能呼叫 f 兩次——昂貴或有副作用的運算式先存變數。

範例輸出

夾在 [0, 100]:
  CLAMP( -20) = 0
  CLAMP(   0) = 0
  CLAMP(  55) = 55
  CLAMP( 100) = 100
  CLAMP( 137) = 100

音量 1.75 夾在 [0.0, 1.0] = 1.00
CLAMP(a - b * 2, 2, 6) = 2(a-b*2 = 1,夾成 2)

練習 4:DECLARE_GETTER —— 用 # 與 ## 生成程式碼(難度:進階)

題目

用 ## 寫巨集 #define DECLARE_GETTER(name),展開成 int get_##name(void) { return name; },為三個全域變數各生成一個 getter; 再用 # 寫 SHOW(expr) 巨集,印出「運算式原文 = 值」。

解題思路

  1. ##(token 連接):把兩段文字黏成一個識別字。 get_##name 代入 width 後變成 get_width——一個全新的函式名。 沒有 ## 的話 get_ name 是兩個 token,根本不是合法函式名。
#define DECLARE_GETTER(name) \
    int get_##name(void) { return name; }

DECLARE_GETTER(width)    /* -> int get_width(void) { return width; } */

注意巨集使用處不加分號:本體已含完整函式定義, 多打分號會多出一個空敘述(檔案層級的空 ; 在 C11 是違規的)。 2. #(字串化):把參數的原始文字變成字串字面值。 SHOW(get_width()) 中 #expr 變成 "get_width()", 與相鄰的 " = %d\n" 靠字串字面值自動接合拼成完整格式字串:

#define SHOW(expr) printf(#expr " = %d\n", (expr))
/* SHOW(x*y) -> printf("x*y" " = %d\n", (x*y)); */
  1. 這類巨集的價值:#/## 能做到函式永遠做不到的事—— 函式拿到的是值,永遠不知道呼叫端寫的「原文」是什麼; 也無法動態生成新的識別字。程式碼生成(一行展開成一個函式) 是大型 C 專案的常見手法(如測試框架、vtable 註冊表)。

參考解答

/*
 * ex4_declare_getter.c
 * --------------------
 * 練習 4:DECLARE_GETTER —— 用 # 與 ## 生成程式碼(難度:進階)
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex4_declare_getter.c
 * 執行:/tmp/out
 */
#include <stdio.h>

/* 三個全域設定值 */
static int width = 800;
static int height = 600;
static int volume = 42;

/*
 * ## 把 get_ 與參數黏成「一個識別字」:
 *   DECLARE_GETTER(width) 展開成
 *   int get_width(void) { return width; }
 */
#define DECLARE_GETTER(name) \
    int get_##name(void) { return name; }

/* 為三個變數各生成一個 getter——一行一個,程式碼自動長出來 */
DECLARE_GETTER(width)
DECLARE_GETTER(height)
DECLARE_GETTER(volume)

/*
 * # 把參數變成「字串字面值」:
 *   SHOW(get_width()) 印出 "get_width() = 800"
 * ——運算式的原始文字與它的值一起印,這是函式做不到的。
 */
#define SHOW(expr) printf(#expr " = %d\n", (expr))

int main(void)
{
    SHOW(get_width());
    SHOW(get_height());
    SHOW(get_volume());

    /* # 對任意運算式都能印出「文字 = 值」 */
    SHOW(get_width() * get_height());

    return 0;
}

程式碼講解

  • 巨集定義用 \ 換行接續——#define 到行尾為止,長本體需要明確續行。
  • DECLARE_GETTER(width) 三行各自展開成一個完整的函式定義, 行尾沒有分號(見思路第 1 點)。想看展開結果,可用 cc -E ex4_declare_getter.c 只跑前置處理器。
  • #expr " = %d\n":兩個相鄰的字串字面值在編譯時自動接合成一個—— 字串化巨集幾乎都靠這招拼格式字串。
  • SHOW 就是 assert 印出條件文字、單元測試框架印出 EXPECT_EQ(a, b) 原文的底層機制——__FILE__/__LINE__ 再配上 #, 一個實用的除錯巨集就完成了(見範例程式 03_predefined_assert.c)。
  • ## 的進階親戚:x ## y 拼變數名可為 swap 巨集生成 「不易撞名」的臨時變數(練習 6 討論)。

範例輸出

get_width() = 800
get_height() = 600
get_volume() = 42
get_width() * get_height() = 480000

練習 5:條件編譯的語言切換(難度:進階)

題目

寫一支程式:用 -DLANG_ZH 編譯時印中文問候、-DLANG_EN 時印英文、 兩者都沒定義時印預設訊息;若兩個旗標同時定義,編譯要直接失敗(#error)。

cc -std=c11 -Wall -Wextra -o /tmp/out ex5_lang_switch.c            # 預設
cc -std=c11 -Wall -Wextra -DLANG_ZH -o /tmp/out ex5_lang_switch.c  # 中文
cc -std=c11 -Wall -Wextra -DLANG_EN -o /tmp/out ex5_lang_switch.c  # 英文

解題思路

  1. 條件編譯 vs 執行期 if:#if 家族在編譯之前決定哪段程式碼存在—— 沒被選中的分支連編譯器都看不到,執行檔裡只有一種語言的字串。 執行期 if 則是三種字串都在執行檔裡、跑的時候挑一個。 平台差異、付費功能開關、除錯碼這類「編譯出不同產品」的需求用前者。
  2. 分支結構:#if defined(LANG_ZH) / #elif defined(LANG_EN) / #else—— 與執行期 if/else if/else 完全同構。defined(NAME) 判斷巨集是否被定義 (-DLANG_ZH 等同在檔案開頭 #define LANG_ZH 1)。
  3. 策略:分支裡只定義 GREETING、LANG_NAME 兩個巨集, main 只寫一份——把「差異」壓縮到最小範圍,而不是三份 main 各印各的。
  4. 防呆:#error 讓非法組合(兩個語言旗標同時開)在編譯期就爆炸, 錯誤訊息由你指定——比默默選一個語言好得多。

參考解答

/*
 * ex5_lang_switch.c
 * -----------------
 * 練習 5:條件編譯的語言切換(難度:進階)
 *
 * 編譯(三種變體):
 *   cc -std=c11 -Wall -Wextra -o /tmp/out ex5_lang_switch.c            # 預設
 *   cc -std=c11 -Wall -Wextra -DLANG_ZH -o /tmp/out ex5_lang_switch.c  # 中文
 *   cc -std=c11 -Wall -Wextra -DLANG_EN -o /tmp/out ex5_lang_switch.c  # 英文
 * 執行:/tmp/out
 */
#include <stdio.h>

/*
 * 條件編譯發生在「編譯之前」:
 * 沒被選中的分支根本不會進入編譯——執行檔裡只有一種語言的字串。
 */
#if defined(LANG_ZH)
#define GREETING "你好,世界!"
#define LANG_NAME "中文(-DLANG_ZH)"
#elif defined(LANG_EN)
#define GREETING "Hello, world!"
#define LANG_NAME "English (-DLANG_EN)"
#else
#define GREETING "(default greeting / 預設問候)"
#define LANG_NAME "未指定語言(預設分支)"
#endif

/* 防呆:兩個語言旗標不該同時開 */
#if defined(LANG_ZH) && defined(LANG_EN)
#error "LANG_ZH 與 LANG_EN 只能定義一個"
#endif

int main(void)
{
    printf("語言設定:%s\n", LANG_NAME);
    printf("問候語 :%s\n", GREETING);
    return 0;
}

程式碼講解

  • #if defined(X) 與 #ifdef X 等價,但 defined() 能用 &&/|| 組合多條件(如防呆那段),所以偏好前者。
  • 差異被壓縮在 8 行的 #if 區塊裡,main 完全不知道語言的存在—— 這個「隔離變異點」的紀律讓多平台程式碼可維護。
  • #error 的驗證:cc -DLANG_ZH -DLANG_EN ... 會停在 error: "LANG_ZH 與 LANG_EN 只能定義一個",執行檔不會產生。
  • 真實世界的同構場景:#if defined(__APPLE__) / #elif defined(__linux__) 的平台分支、#ifdef NDEBUG 的除錯開關——本題的骨架直接可以套用。

範例輸出

$ cc -std=c11 -Wall -Wextra -o /tmp/out ex5_lang_switch.c && /tmp/out
語言設定:未指定語言(預設分支)
問候語 :(default greeting / 預設問候)

$ cc -std=c11 -Wall -Wextra -DLANG_ZH -o /tmp/out ex5_lang_switch.c && /tmp/out
語言設定:中文(-DLANG_ZH)
問候語 :你好,世界!

$ cc -std=c11 -Wall -Wextra -DLANG_EN -o /tmp/out ex5_lang_switch.c && /tmp/out
語言設定:English (-DLANG_EN)
問候語 :Hello, world!

練習 6:swap(t, x, y) 巨集(難度:挑戰)

題目

(K&R 習題 4-14)寫巨集 #define swap(t, x, y) 交換兩個 t 型別的引數。 要求:在 if/else 中使用不會出錯;對 int、double、const char * 三種型別都能工作;說明臨時變數的名稱衝突問題。

解題思路

  1. 把型別當參數是這題的亮點:函式的參數只能是值, 巨集的參數是文字——連 int、double 這種型別名都能傳。 一個巨集服務所有型別,這是 C 裡最接近 C++ template 的東西。
  2. 需要臨時變數 → 需要一個區塊:
{ t tmp = (x); (x) = (y); (y) = tmp; }

但裸區塊或多敘述巨集有經典陷阱(README 陷阱 4):

if (a > b) SWAP_BAD(x, y);   /* 若巨集是三個敘述,只有第一句受 if 管! */
else ...                      /* 裸區塊 {} 後面加分號,else 會配對失敗 */
  1. do { ... } while (0) 慣用法:把整個本體包起來, 它是單一敘述且期待一個分號——放進 if/else 的任何位置, 行為都與一個函式呼叫語句一致。這是所有多敘述巨集的標準包裝。
  2. 名稱衝突:巨集是文字取代——如果呼叫端剛好有一個變數叫 tmp, swap(int, tmp, other) 展開後 t tmp = (tmp); 就是自己初始化自己,壞掉。 緩解手段: - 取罕見名字(本解用 swap_tmp_); - 或 K&R 提示的 x ## y 拼出如 xy 的名字(仍可能撞,且 x、y 是運算式時根本拼不出合法識別字)。 文字取代沒有完美解(GCC 擴充的 __typeof__ + 語句運算式才能根治)—— 認識極限也是本題的教學目標。

參考解答

/*
 * ex6_swap_macro.c
 * ----------------
 * 練習 6:swap(t, x, y) 巨集(難度:挑戰)
 * 出處:K&R 習題 4-14
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex6_swap_macro.c
 * 執行:/tmp/out
 */
#include <stdio.h>

/*
 * 交換兩個 t 型別的變數:
 * - 用 do { ... } while (0) 包裹,讓巨集在 if/else 裡也安全;
 * - 臨時變數取罕見的名字 swap_tmp_,降低與呼叫端撞名的機率
 *   (若呼叫端剛好也有 swap_tmp_,巨集就壞了——這是文字取代的天生限制)。
 */
#define swap(t, x, y)          \
    do {                       \
        t swap_tmp_ = (x);     \
        (x) = (y);             \
        (y) = swap_tmp_;       \
    } while (0)

int main(void)
{
    /* 型別當「參數」——這是函式做不到、巨集才有的泛型效果 */
    int a = 1, b = 2;
    swap(int, a, b);
    printf("int    :a = %d, b = %d\n", a, b);

    double x = 3.14, y = 2.71;
    swap(double, x, y);
    printf("double :x = %.2f, y = %.2f\n", x, y);

    const char *s = "hello", *t = "world";
    swap(const char *, s, t);
    printf("字串   :s = %s, t = %s\n", s, t);

    /* do-while(0) 的價值:放在 if/else 裡不會破功 */
    if (a > b)
        swap(int, a, b);      /* 分號接在 while(0) 後,語法完全正確 */
    else
        printf("a <= b,不交換\n");
    printf("if/else 測試後:a = %d, b = %d(a>b 成立,又換回 1, 2)\n", a, b);

    return 0;
}

程式碼講解

  • do { ... } while (0):迴圈只跑一次,編譯器會完全優化掉—— 它存在的意義純粹是語法:讓「多敘述」偽裝成「一個期待分號的敘述」。 對照壞寫法:裸 {} 在 if (…) swap(…); else 中, }; 的分號是多餘敘述,else 找不到 if 而編譯錯誤。
  • swap(const char *, s, t):交換的是指標,兩個字串字面值本身不動—— 結合第 02 單元的知識,指標交換是 O(1),字串內容交換是 O(n)。
  • if (a > b) swap(int, a, b); 這行同時測了兩件事: do-while(0) 的語法安全(有 else 的 if 裡放巨集不會編譯錯誤), 以及交換過的 a=2, b=1 走進 if 分支再換回 1, 2——由最後一行印出驗證。
  • 撞名示範(自己試試):在 main 宣告 int swap_tmp_ = 99; 再 swap(int, swap_tmp_, a);——展開後 int swap_tmp_ = (swap_tmp_); 自己初始化自己(clang 開 -Wuninitialized 會警告 self-initialization)。

範例輸出

int    :a = 2, b = 1
double :x = 2.71, y = 3.14
字串   :s = world, t = hello
if/else 測試後:a = 1, b = 2(a>b 成立,又換回 1, 2)

常見錯誤解法對照

錯誤寫法 症狀 正確做法
#define ABS(x) x < 0 ? -x : x 10 - ABS(-2) 安靜算錯 參數個個加括號、本體整體加括號
對函式參數用 ARRAY_LEN 永遠回 2(指標大小/元素大小) 在陣列定義處算好長度,當參數傳入
ABS(i++)、CLAMP(f(), …) 副作用/昂貴呼叫重複求值 先存變數再傳;或改用函式
DECLARE_GETTER(width);(多分號) 檔案層級多出空敘述(違規) 本體含完整定義的巨集,使用處不加分號
多敘述巨集用裸 {} 或不包裹 if/else 配對錯亂、編譯錯誤 do { ... } while (0) 包裹
#define N 100;(結尾分號) int a[N] 展開成 a[100;] 編譯錯誤 巨集定義不加分號(文字照抄)
兩個互斥旗標同時 -D 沒防呆 默默選中第一個分支 #if defined(A) && defined(B) + #error