巨集 是 C 語言預處理器 提供的文字替換機制。它不是函數,不進行型別檢查,不產生函數呼叫開銷,但在系統程式、嵌入式開發、高效能計算中無處不在。正確掌握巨集寫法、理解展開規則、避開經典陷阱,是進階 C 程式設計師的必修課。
// 正確:雙層括號保護
#define MAX(a,b) (((a) > (b)) ? (a) : (b))
#define MIN(a,b) (((a) < (b)) ? (a) : (b))
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
// 多行巨集:反斜線換行,末行無反斜線
#define SWAP(type, a, b) \
do { \
type _tmp = (a); \
(a) = (b); \
(b) = _tmp; \
} while (0)
| 維度 | 巨集 | 函數 |
|---|---|---|
| 處理階段 | 預處理 (文字替換) | 編譯/連結 (機器碼) |
| 型別檢查 | 無 | 強型別 |
| 執行開銷 | 零 (內聯展開) | 呼叫/返回、棧框架 |
| 除錯可見性 | 差 (原始碼不見巨集體) | 佳 (符號表、斷點) |
| 遞迴支援 | 無 | 支援 |
| 適用場景 | 常數、簡單運算、程式碼生成、條件編譯 | 複雜邏輯、需型別安全、遞迴、庫 API |
關鍵結論:巨集「不產生中斷/呼叫」,直接在呼叫點展開。這帶來極致效能,但也意味著引數會被多次求值 (副作用風險) 與程式碼膨脹。
#define MAX(a,b) ((a) > (b) ? (a) : (b))
int i = 0;
int m = MAX(i++, 5); // 展開: ((i++) > 5 ? (i++) : 5) -> i 增加兩次!
解法:改用 `inline` 函數 (C99) 或 GNU 延伸的 `({ ... })` 陳述式表達式:
// C99 inline (編譯器決定是否內聯)
static inline int max_int(int a, int b) { return a > b ? a : b; }
// GCC/Clang 延伸:單次求值巨集
#define MAX_SAFE(a,b) ({ \
typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a > _b ? _a : _b; })
#define SQUARE(x) x * x
int y = SQUARE(3 + 2); // 展開: 3 + 2 * 3 + 2 = 11 (錯!預期 25)
// 正確: #define SQUARE(x) ((x) * (x))
#define LOG(msg) printf("%s\n", msg)
if (error) LOG("fail"); else LOG("ok");
// 展開: if (error) printf(...); ; else printf(...); // 語法錯誤:多餘分號切斷 else
// 正確: #define LOG(msg) do { printf("%s\n", msg); } while (0)
#define FOO FOO + 1 // 預處理器偵測到遞迴,停止展開,保留 FOO
#define STR(x) #x
#define XSTR(x) STR(x) // 兩層展開參數
#define ASSERT(cond) \
do { if (!(cond)) fprintf(stderr, "Assertion failed: %s, file %s, line %d\n", \
XSTR(cond), __FILE__, __LINE__); } while (0)
ASSERT(ptr != NULL); // 展開字串 "ptr != NULL"
// 自動生成結構體/函數名
#define DECLARE_HANDLER(name) \
void handle_##name(int arg); \
void handle_##name(int arg) { printf("Handling %s: %d\n", #name, arg); }
DECLARE_HANDLER(connect)
DECLARE_HANDLER(disconnect)
// 生成: handle_connect, handle_disconnect
#define LOG(fmt, ...) fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
// GNU 延伸: ##__VA_ARGS__ 當 __VA_ARGS__ 為空時吃掉前面的逗號
LOG("Init done"); // OK
LOG("Value: %d", 42); // OK
#define BIT(nr) (1UL << (nr))
#define BIT_MASK(nr) (BIT(nr) - 1)
#define SET_BIT(var, bit) ((var) |= BIT(bit))
#define CLR_BIT(var, bit) ((var) &= ~BIT(bit))
#define TST_BIT(var, bit) ((var) & BIT(bit))
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))
// 用法: 從 list_node 指標反推包含結構體指標
struct list_head { struct list_head *next, *prev; };
struct my_struct { int data; struct list_head node; };
struct list_head *pos = ...;
struct my_struct *p = container_of(pos, struct my_struct, node);
#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))
// 條件為真 -> 陣列大小負數 -> 編譯錯誤
BUILD_BUG_ON(sizeof(int) != 4); // 確保 int 為 4 bytes
掌握巨集精髓,不是要到處使用它,而是知道何時它是唯一或最佳工具,並能寫出不出錯的巨集。這正是 C 程式設計師從「會用語法」進化為「駕馭語言」的分水嶺。