C/C++ 預處理器提供兩個強大的標記運算符:#(字符串化)和 ##(標記粘連/連接)。它們讓宏能夠在預編譯期操作標記本身,實現元編程能力,廣泛用於日誌、序列化、代碼生成等場景。
| 運算符 | 名稱 | 功能 | 位置 |
|---|---|---|---|
# | 字符串化 | 將宏參數轉換為字符串字面量 | 宏定義體中,僅能作用於參數 |
## | 標記粘連 | 將兩側標記合併為單個標記 | 宏定義體中,可作用於參數或普通標記 |
#define MZ_PRINT(n) printf("x" #n "=%d", x##n)
int x1 = 1;
MZ_PRINT(1);
// 預處理後展開為:
// printf("x" "1" "=%d", x1);
// 即:printf("x1=%d", x1);
#n → "1"(參數 1 字符串化)x##n → x1(標記 x 與參數 1 粘連)"x" "1" "=%d" → "x1=%d"## 兩側的參數不會先展開,直接粘連原始標記。若需先展開參數再粘連,需引入輔助宏:
#define CONCAT(a, b) a##b
#define EXPAND_CONCAT(a, b) CONCAT(a, b)
#define FOO 123
EXPAND_CONCAT(FOO, BAR) // → 123BAR
CONCAT(FOO, BAR) // → FOOBAR (參數未展開)
#define STR(x) #x
#define VAL 42
STR(VAL) // → "VAL" 而非 "42"
STR(VAL+1) // → "VAL+1"
同樣需雙層宏強制展開:
#define XSTR(x) STR(x)
XSTR(VAL) // → "42"
C99 可變參數宏 __VA_ARGS__ 配合 ## 可消除尾隨逗號:
#define LOG(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
LOG("hello") // → fprintf(stderr, "hello") // ## 吃掉逗號
LOG("val=%d", 42) // → fprintf(stderr, "val=%d", 42)
#define PASTE(a, b) a##b
PASTE(123, 456) // → 123456 (合法預處理數字)
PASTE(+, -) // → +- (非法標記,編譯錯誤)
#define DECLARE_GETTER(type, name) \
type get_##name() const { return m_##name; }
class Foo {
int m_id;
float m_score;
public:
DECLARE_GETTER(int, id)
DECLARE_GETTER(float, score)
};
// 展開為 get_id(), get_score()
#define ENUM_TO_STR(e) case e: return #e;
const char* error_str(int code) {
switch(code) {
ENUM_TO_STR(E_OK)
ENUM_TO_STR(E_NOMEM)
ENUM_TO_STR(E_INVAL)
default: return "UNKNOWN";
}
}
#define COMPILE_ASSERT(cond, msg) typedef char static_assert_##msg[(cond)?1:-1]
COMPILE_ASSERT(sizeof(int) == 4, int_must_be_32bit);
#define OP_LIST \
X(ADD, +) \
X(SUB, -) \
X(MUL, *)
#define X(name, op) name,
enum Op { OP_LIST };
#undef X
#define X(name, op) #op,
const char* op_str[] = { OP_LIST };
#undef X
gcc -E -P file.c 查看預處理輸出。cl /E /P file.c 生成 .i 文件。# 與 ## 是預處理器的「元編程雙子星」。善用它們能極大減少樣板代碼、實現領域特定語言(DSL)風格接口,但濫用會導致代碼難讀、難調試。原則:能用語言本身機制(模板、內聯函數、constexpr)解決時,優先不用宏;必須用宏時,保持定義簡潔、文檔完善、命名規範(如大寫+前綴)。