運算符核心語義
| 運算符 | 英文名 | 功能 | 作用階段 |
# | Stringification (字符串化) | 將宏參數轉為字符串字面量 | 宏擴展時 |
## | Concatenation (連接/粘合) | 將兩側 Token 拼接為單一 Token | 宏擴展時 |
基礎演示程序
#include
void quit_command() { printf("I am quit command\n"); }
void help_command() { printf("I am help command\n"); }
struct command {
char *name;
void (*function)(void);
};
// 核心宏:字符串化 name,連接生成函數名
#define COMMAND(NAME) { #NAME, NAME##_command }
#define PRINT(NAME) printf("token" #NAME "=%d\n", token##NAME)
int main() {
int token9 = 9;
PRINT(9); // token9=9
struct command commands[] = {
COMMAND(quit), // {"quit", quit_command}
COMMAND(help) // {"help", help_command}
};
commands[0].function(); // I am quit command
return 0;
}
展開過程逐步解析
// PRINT(9)
printf("token" #9 "=%d\n", token##9)
→ printf("token" "9" "=%d\n", token9) // 字符串字面量自動拼接
→ printf("token9=%d\n", token9)
// COMMAND(quit)
{ #quit, quit##_command }
→ { "quit", quit_command }
進階模式庫
1. 自動註冊命令表(解析器雛形)
#define REG_CMD(name) { #name, cmd_##name }
struct cmd_entry { const char *name; void (*fn)(void); } cmd_table[] = {
REG_CMD(init),
REG_CMD(start),
REG_CMD(stop),
REG_CMD(status)
};
// 查找執行
void execute(const char *input) {
for (size_t i=0; i
2. 結構體偏移量計算(Linux 內核 container_of 原理)
#define OFFSETOF(type, member) ((size_t)&(((type*)0)->member))
#define CONTAINER_OF(ptr, type, member) ((type*)((char*)(ptr) - OFFSETOF(type, member)))
3. 編譯期斷言(C11 _Static_assert 前身)
#define COMPILE_ASSERT(cond, msg) typedef char static_assert_##msg[(cond)?1:-1]
COMPILE_ASSERT(sizeof(int) == 4, int_must_be_4_bytes);
// 失敗時:error: size of array 'static_assert_int_must_be_4_bytes' is negative
4. 可變參數調試宏(GNU ##__VA_ARGS__ 擴展)
#ifdef DEBUG
#define DBG(fmt, ...) fprintf(stderr, "[DBG %s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#else
#define DBG(fmt, ...) ((void)0)
#endif
DBG("User %s login", name); // 正常
DBG("Checkpoint"); // 無參數也合法(## 吞噬逗號)
兩層宏展開技巧(必須掌握)
當參數本身為宏時,#/## 旁邊不自動展開:
#define VAL 42
#define STR(s) #s
#define STR2(s) STR(s)
STR(VAL) → "VAL" (參數未展開)
STR2(VAL) → STR(42) → "42" (兩層強制展開)
常見陷阱避坑表
| 陷阱 | 錯誤代碼 | 正確代碼 | 原因 |
| 尾隨逗號 | #define M(...) f(__VA_ARGS__) | #define M(...) f(##__VA_ARGS__) | GNU擴展,空參數吞噬逗號 |
| 參數未展開 | STR(MACRO) | STR2(MACRO) | 兩層宏強制展開 |
| 非法Token拼接 | INT##_MAX | 直接寫 INT_MAX | 拼接結果必須合法 |
| 副作用重複 | #define SQUARE(x) x*x | 內聯函數或 ({typeof(x)_x=x;_x*_x;}) | 參數多次出現導致多次求值 |
| 字符串化空白 | STR( a ) → " a " | 調用時不加多餘空格 | 參數原樣保留空白 |
最佳實踐總結
- 宏非函數:無類型檢查、無作用域、參數多次展開、調試困難。
- 能用
static inline 就不用宏:類型安全、可調試、零開銷(優化後)。
- 必須用宏的場景:
__FILE__/__LINE__ 捕獲、Token 拼接生成標識符、字符串化參數名、編譯期斷言、可變參數日志。
- 規範命名:宏全大寫,內部宏加
_ 前綴(_STR、_CONCAT)。
- 文檔化副作用:註釋明確標註參數求值次數、是否修改參數。