C 預處理器中的 # (字符串化)、## (標記粘連)、#@ (字符化,GNU 擴展) 是元編程的核心工具。本文系統講解三者語法規則、**參數展開順序陷阱**、**中間轉換宏模式**,並給出 Linux 內核、Minix 等頂級項目中的工程級應用案例。
| 運算符 | 名稱 | 作用 | 示例 | 展開結果 |
|---|---|---|---|---|
# |
字符串化 | 將參數轉為字符串字面量 | #define STR(x) #xSTR(hello) |
"hello" |
## |
標記粘連 | 將兩側標記合併為單一標記 | #define CAT(a,b) a##bCAT(var, 1) |
var1 |
#@ |
字符化 | 將參數轉為字符常量 (GNU 擴展) | #define CHAR(c) #@cCHAR(z) |
'z' |
# x、a ## b 均合法。
#define A (2)
#define STR(s) #s
#define CONS(a,b) int(a##e##b)
STR(INT_MAX) // → "INT_MAX" ❌ 期望 "2147483647"
CONS(A, A) // → int(AeA) ❌ 期望 int((2)e(2)) → 200
#define A (2)
// 字符串化
#define _STR(s) #s
#define STR(s) _STR(s) // 關鍵:多加一層,強制參數在這層先展開
// 粘連
#define _CONS(a,b) int(a##e##b)
#define CONS(a,b) _CONS(a,b)
STR(INT_MAX) // → STR(2147483647) → _STR(2147483647) → "2147483647" ✅
CONS(A, A) // → CONS((2), (2)) → _CONS((2), (2)) → int((2)e(2)) → 200 ✅
原理:宏展開時,參數在**傳入宏定義前**先完全展開。中間宏 _STR/_CONS 接收的是已展開的標記序列,再應用 #/## 即可得到預期結果。
__LINE__ + ##)
#define ___ANONYMOUS1(type, var, line) type var##line
#define __ANONYMOUS0(type, line) ___ANONYMOUS1(type, _anonymous, line)
#define ANONYMOUS(type) __ANONYMOUS0(type, __LINE__)
// 使用
ANONYMOUS(static int); // → static int _anonymous42; (假設在第 42 行)
// 三層展開保證 __LINE__ 在第二層被解析為數字標記,再與 _anonymous 粘連
#define FILL(a) {a, #a}
enum IDD { OPEN, CLOSE };
typedef struct { IDD id; const char *msg; } MSG;
MSG _msg[] = { FILL(OPEN), FILL(CLOSE) };
// 展開為:{ {OPEN, "OPEN"}, {CLOSE, "CLOSE"} }
// 優勢:增刪枚舉值時,數組自動同步,消除手工維護錯誤
#define _GET_FILE_NAME(f) #f
#define GET_FILE_NAME(f) _GET_FILE_NAME(f) // 轉換宏
static char FILE_NAME[] = GET_FILE_NAME(__FILE__);
// → static char FILE_NAME[] = "driver.c";
// 用途:日志、斷言、調試輸出自動攜帶文件名
#define _TYPE_BUF_SIZE(type) sizeof #type
#define TYPE_BUF_SIZE(type) _TYPE_BUF_SIZE(type)
char buf[TYPE_BUF_SIZE(INT_MAX)];
// INT_MAX → 2147483647 → #type → "2147483647" → sizeof → 11 (含 \0)
// buf 大小編譯期確定為 11,零開銷
#ifdef _ANSI
#define __str(x) #x
#define __xstr(x) __str(x) // 轉換宏
#define assert(expr) ((expr) ? (void)0 : \
__bad_assertion("Assertion \"" #expr "\" failed, file " \
__xstr(__FILE__) ", line " __xstr(__LINE__) "\n"))
#endif
// __xstr(__FILE__) → __str("driver.c") → "\"driver.c\""
// 實現了文件名、行號、表達式文本的完整編譯期捕獲
#define STR(s) _STR(s) / #define CAT(a,b) _CAT(a,b)。## 粘連後必須形成**合法預處理標記**,否則編譯錯誤 (如 CAT(+, -) → +- 非法)。#@ 為 **GNU 擴展**,非標準 C,**跨編譯器項目禁用**,改用 'x' 字面量或 (char)'x'。#/## 不求值,但外層調用可能多次求值。gcc -E / clang -E 查看預處理輸出,#pragma message 運行期輸出。#、##、#@ 是 C 預處理器將「文本操作」提升為「語法級元編程」的三把鑰匙。掌握**參數展開順序**、**中間轉換宏模式**、以及**標記合法性約束**,即可安全構建類型安全的枚舉映射、編譯期字符串計算、自動化代碼生成等高階基礎設施。Linux 內核、Minix、嵌入式框架中的大量經典用法,正是這三個運算符威力的最佳註腳。