本檔收錄本單元的練習題、完整解題思路與參考解答。
強烈建議先自己動手做:看完題目就闔上這份檔案,寫出你的版本、編譯、執行、卡住了再回來看「解題思路」;
真的做完了,再對照「參考解答」與「程式碼講解」。每題的完整程式碼也放在 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) 為什麼算錯。
解題思路
- 巨集只是文字取代——前置處理器不懂優先權、不懂型別,
它只是把
ABS(某文字)換成定義裡的樣子。所以安全守則是機械性的: 每個參數出現處都加括號,整個本體再加一層括號。
#define ABS(x) ((x) < 0 ? -(x) : (x))
- 壞版本壞在哪?
#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])),用它走訪一個陣列;
再示範並解釋:為什麼它對「函式參數的陣列」會失效?
解題思路
- 原理:陣列的總位元組數除以單一元素的位元組數 = 元素個數。
sizeof是編譯期運算子,整個巨集在編譯期就變成常數,零執行成本。(a)[0]的寫法讓元素型別改變時巨集自動跟上——不用寫死sizeof(int)。 - 它失效的唯一場景,也是最容易踩的:陣列一旦傳進函式就退化(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)
- 編譯器其實認得這個經典錯誤:直接在函式裡寫
ARRAY_LEN(a), clang 會發出-Wsizeof-pointer-div警告。解答中拆成兩步計算 (先取sizeof a、再相除)讓錯誤的來源看得一清二楚。 - 結論與正確做法:元素個數必須在定義陣列的作用域(陣列還是陣列的地方)
用
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),
並驗證引數是運算式時仍正確。
解題思路
- 邏輯只有三分支,巢狀條件運算子一行寫完:
x < lo -> lo
x > hi -> hi
其他 -> x
#define CLAMP(x, lo, hi) ((x) < (lo) ? (lo) : ((x) > (hi) ? (hi) : (x)))
- 括號:
?:是右結合,內層?:不加括號其實也會正確配對, 但明確括起來可讀性與安全性都更好;三個參數的每次出現照例全部括號。 - 為什麼用巨集而不是函式? 這題正好是「巨集泛型」的展示場:
同一個
CLAMP直接用在int和double,函式版本得寫兩個 (clamp_i、clamp_d)。代價照舊:x在本體出現三次, 不能傳CLAMP(x++, 0, 100)。 - 測資:邊界值(恰好 = 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) 巨集,印出「運算式原文 = 值」。
解題思路
##(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)); */
- 這類巨集的價值:
#/##能做到函式永遠做不到的事—— 函式拿到的是值,永遠不知道呼叫端寫的「原文」是什麼; 也無法動態生成新的識別字。程式碼生成(一行展開成一個函式) 是大型 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 # 英文
解題思路
- 條件編譯 vs 執行期 if:
#if家族在編譯之前決定哪段程式碼存在—— 沒被選中的分支連編譯器都看不到,執行檔裡只有一種語言的字串。 執行期if則是三種字串都在執行檔裡、跑的時候挑一個。 平台差異、付費功能開關、除錯碼這類「編譯出不同產品」的需求用前者。 - 分支結構:
#if defined(LANG_ZH)/#elif defined(LANG_EN)/#else—— 與執行期 if/else if/else 完全同構。defined(NAME)判斷巨集是否被定義 (-DLANG_ZH等同在檔案開頭#define LANG_ZH 1)。 - 策略:分支裡只定義
GREETING、LANG_NAME兩個巨集,main只寫一份——把「差異」壓縮到最小範圍,而不是三份 main 各印各的。 - 防呆:
#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 *
三種型別都能工作;說明臨時變數的名稱衝突問題。
解題思路
- 把型別當參數是這題的亮點:函式的參數只能是值,
巨集的參數是文字——連
int、double這種型別名都能傳。 一個巨集服務所有型別,這是 C 裡最接近 C++ template 的東西。 - 需要臨時變數 → 需要一個區塊:
{ t tmp = (x); (x) = (y); (y) = tmp; }
但裸區塊或多敘述巨集有經典陷阱(README 陷阱 4):
if (a > b) SWAP_BAD(x, y); /* 若巨集是三個敘述,只有第一句受 if 管! */
else ... /* 裸區塊 {} 後面加分號,else 會配對失敗 */
do { ... } while (0)慣用法:把整個本體包起來, 它是單一敘述且期待一個分號——放進 if/else 的任何位置, 行為都與一個函式呼叫語句一致。這是所有多敘述巨集的標準包裝。- 名稱衝突:巨集是文字取代——如果呼叫端剛好有一個變數叫
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 |