在帶參數的宏定義中,##(連接符/粘合運算符)將兩側的預處理 Token拼接為單一 Token。拼接發生在參數替換之後、宏擴展之前。
A ## B、A##B、A ##B 效果相同。123 ## 456 非合法數字),行為未定義。C99 引入 __VA_ARGS__ 支持可變參數宏,但直接使用存在尾隨逗號問題:
#define DEBUG(fmt, args...) fprintf(stderr, fmt, args)
DEBUG("Test\n");
// 展開為:fprintf(stderr, "Test\n", ) ← 語法錯誤:多餘逗號
##__VA_ARGS__
#define DEBUG(fmt, args...) fprintf(stderr, fmt, ##args)
DEBUG("Test\n");
// 展開為:fprintf(stderr, "Test\n") ← 逗號被 ## 吞噬,合法!
DEBUG("Value: %d\n", 42);
// 展開為:fprintf(stderr, "Value: %d\n", 42) ← 正常保留參數
機制:當 __VA_ARGS__ 為空時,## 會「吃掉」前面的逗號;非空時正常拼接參數。此為 GCC/Clang/MSVC 通用擴展,非標準 C,但廣泛支持。
#define DEBUG(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)
// 調用必須至少有一個可變參數:DEBUG("Test\n", (void)0)
#define DECLARE_VAR(type, name) type name##_var
#define CONCAT(a, b) a##b
DECLARE_VAR(int, foo); // → int foo_var;
CONCAT(test, _case); // → test_case
#define ANONYMOUS_VAR(type) type _anonymous_##__LINE__
// 每行展開唯一:int _anonymous_42; int _anonymous_43;
#define FIELD_OFFSET(type, field) ((size_t)&(((type*)0)->field))
#define CONTAINER_OF(ptr, type, field) ((type*)((char*)(ptr) - FIELD_OFFSET(type, field)))
// Linux 內核經典容器宏,依賴 Token 拼接計算偏移
# 將參數轉為字符串字面量,## 拼接 Token,兩者常配合使用:
#define _STR(x) #x
#define STR(x) _STR(x) // 兩層展開參數
#define ASSERT(cond) \
do { \
if (!(cond)) { \
fprintf(stderr, "Assertion failed: %s, file %s, line %d\n", \
STR(cond), __FILE__, __LINE__); \
abort(); \
} \
} while(0)
ASSERT(x > 0);
// 展開:STR(x > 0) → _STR(x > 0) → "x > 0"
#define LOG(fmt, ...) \
printf("[%s:%d %s] " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__)
LOG("Connection established");
// [main.c:42 main] Connection established
宏參數在#或##操作符旁邊時不會先展開,需通過中間宏強制展開:
#define VAL 123
#define TO_STR(x) #x
#define TO_STR_EXPANDED(x) TO_STR(x)
TO_STR(VAL) // → "VAL" (參數未展開)
TO_STR_EXPANDED(VAL) // → "123" (VAL 先展開為 123,再字符串化)
// 通用調試宏,支持可變參數、自動換行、發布版禁用
#ifdef NDEBUG
#define DBG(fmt, ...) ((void)0)
#else
#define DBG(fmt, ...) \
fprintf(stderr, "[DEBUG %s:%d %s()] " fmt "\n", \
__FILE__, __LINE__, __func__, ##__VA_ARGS__)
#endif
// 使用示例
DBG("User %s logged in, id=%d", username, uid);
DBG("Checkpoint reached"); // 無可變參數也合法
| 陷阱 | 錯誤代碼 | 正確代碼 | 說明 |
|---|---|---|---|
| 尾隨逗號 | #define M(a,...) f(a,__VA_ARGS__) | #define M(a,...) f(a,##__VA_ARGS__) | GNU 擴展,空參數時吞噬逗號 |
| 參數未展開 | #define S(x) #x; S(MACRO) | #define _S(x) #x; #define S(x) _S(x) | 兩層宏強制參數先展開 |
| 非法 Token 拼接 | INT##_MAX | INT_MAX 直接寫 | 拼接結果必須為合法 Token |
| 副作用重複求值 | #define MAX(a,b) ((a)>(b)?(a):(b)) | 使用內聯函數或 GCC ({typeof(a) _a=(a);_b=(b);_a>_b?_a:_b;}) | 宏參數多次出現導致副作用重複 |
## 連接符是 C 預處理器元編程的核心工具,掌握其與 #、__VA_ARGS__、多層展開的協作模式,能構建類型安全、可讀性強、零開銷的調試與通用工具宏。記住:宏非函數,參數無類型,展開無作用域,副作用極危險——關鍵路徑優先用 static inline 函數替代。