C 語言巨集(宏)不僅是常量替換工具,`#`(字串化)、`##`(符號連接/粘貼)、`#@`(字元化)三大預處理運算子能實現代碼生成、除錯輔助、變量名自動化等高階技巧。本文深入解析其語義、編譯器差異與實戰模式。
| 運算子 | 名稱 | 功能 | 標準支援 |
|---|---|---|---|
# | 字串化 | 將宏參數轉為字串字面量 | ANSI C (C89) |
## | 符號連接 | 連接兩個符號生成新符號 | ANSI C (C89) |
#@ | 字元化 | 將單字元參數轉為字元常量 | 非標準 (GCC/舊版 MSVC 支援) |
#define CONCAT(x, y) x ## y
int CONCAT(a, b); // 展開為: int ab;
printf(CONCAT("ab", "cd")); // 展開為: printf("abcd");
#define DECLARE_VAR(prefix, name) int prefix ## _ ## name
DECLARE_VAR(MODULE_A, counter); // int MODULE_A_counter;
DECLARE_VAR(MODULE_A, buffer); // int MODULE_A_buffer;
統一修改前綴只需改宏調用處,消除手動重命名錯誤。
#define M 0
#define VAR(x) Var_##x
int VAR(M); // 標準 C: 展開為 Var_M (參數不遞歸展開)
// 部分舊編譯器 (如 TC 2.0): 可能展開為 Var_0
規則:`##` 連接的是參數符號本身,不遞歸展開參數內容。需間接宏強制展開:
#define VAR_INDIRECT(x) VAR(x)
#define VAR(x) Var_##x
int VAR_INDIRECT(M); // 先展開 M -> 0,再連接 -> Var_0
#define TO_STRING(x) #x
printf(TO_STRING(variable_name)); // 輸出: "variable_name"
printf(TO_STRING(Hello World)); // 輸出: "Hello World" (空格保留)
#define CHECK_VAR(x, fmt) printf(#x " = " #fmt "\n", x)
int count = 42;
CHECK_VAR(count, %d); // 展開為: printf("count = %d\n", count);
// 輸出: count = 42
#define STR(x) #x
#define XSTR(x) STR(x)
#define VERSION 1.2.3
printf(XSTR(VERSION)); // 輸出: "1.2.3" 而非 "VERSION"
非標準,僅部分編譯器支援(GCC、舊版 MSVC),現代代碼不建議使用。
#define TO_CHAR(x) #@x
char c = TO_CHAR(a); // 'a'
char d = TO_CHAR(1); // '1'
char e = TO_CHAR(abc); // 'c' (取最後一字元,超過 4 字元報錯)
標準替代方案:`#define TO_CHAR(x) #x[0]` 配合字串化,或直接寫字元常量。
C 標準規定:相鄰字串字面量自動連接,無需任何運算子。
printf("Hello " "World"); // 等同於 "Hello World"
puts(TO_STRING(identifier) "\n"); // 字串化結果直接接換行符
此特性在編譯期完成,零運行開銷,優於運行時 `strcat`。
#define CTRL(c) ('c' & 0x1F) // 錯誤!'c' 是字元常量,宏參數 c 不展開
// 正確寫法:利用字元運算
#define CTRL(c) ((c) & 0x1F)
CTRL('d') // -> ('d' & 0x1F) = 4
ANSI C 規定:字串/字元常量內的宏參數不展開。舊編譯器可能違反此規則,導致代碼不可移植。
掌握預處理器元編程能力,能讓 C 代碼在保持零開銷的前提下獲得驚人的表達力與可維護性。