在 C/C++ 前處理器中,# 號運算子(Stringizing Operator)是一個鮮為人知但極其實用的功能:它能將宏參數「原封不動」地轉換為字串常值。這在除錯訊息、日誌記錄、序列化等場合極為方便,能避免重複輸入字串字面值導致的維護困擾。
#define SymbolToString(s) #s
當呼叫 SymbolToString(ANY_TOKEN) 時,前處理器會將 ANY_TOKEN 替換為 "ANY_TOKEN"(含雙引號)。注意:
傳統寫法需手寫 switch-case 對應每個列舉值,易出錯且難維護。善用 # 運算子即可一勞永逸:
#define EnumToStr(e) #e
enum Value { ONE, TWO, THREE };
void LogValue(Value v)
{
switch (v)
{
case ONE: std::cout << EnumToStr(ONE) << std::endl; break;
case TWO: std::cout << EnumToStr(TWO) << std::endl; break;
case THREE: std::cout << EnumToStr(THREE) << std::endl; break;
}
}
// 輸出: ONE / TWO / THREE (不含引號)
結合 __FUNCTION__ (C99/C++11) 或 __FUNCSIG__ (MSVC),可建立強大的除錯巨集:
#define SymbolToStr(s) #s
// 除錯輸出巨集:自動包含檔案、行號、函式名、變數名與值
#define DEBUG_LOG_VAR(var) \
OutputDebugStringA( \
(std::string(__FILE__) + ":" + std::to_string(__LINE__) + " " \
+ __FUNCTION__ + " " + SymbolToStr(var) + " = " \
+ std::to_string(var) + "\n").c_str() )
// 使用範例
void ProcessData(int count, double ratio)
{
DEBUG_LOG_VAR(count); // 輸出: source.cpp:42 ProcessData count = 10
DEBUG_LOG_VAR(ratio); // 輸出: source.cpp:43 ProcessData ratio = 1.5
}
若宏參數本身也是巨集,需透過雙層巨集強制先展開再字串化:
#define _STR(s) #s
#define STR(s) _STR(s)
#define VERSION 1.2.3
const char* version_str = STR(VERSION); // 結果: "1.2.3" 而非 "VERSION"
| 場景 | 範例 | 優勢 |
|---|---|---|
| Enum/常數除錯輸出 | case ERR_OK: log(#ERR_OK); | 修改列舉值自動同步字串,零維護成本 |
| 單元測試斷言訊息 | ASSERT_MSG(cond, #cond) | 失敗時直接顯示條件運算式原文 |
| 序列化/反射標籤 | REGISTER_FIELD(name) | 欄位名稱與程式碼同步,避免字串硬編碼 |
| 自動產生字串對照表 | #define MAP_ENTRY(k) {k, #k} | 一次定義,編譯期生成查找表 |
#var 僅在前處理階段對宏參數生效。#func(),不會執行函式,僅產生字串 "func()"。善用 # 字串化運算子,能大幅減少重複字串字面值的撰寫,提升程式碼可維護性與除錯效率。配合 ## (Token Pasting) 與可變參數巨集 (C99 __VA_ARGS__),更能構建領域專用的元程式工具。下次撰寫 switch-case 對應列舉字串時,不妨嘗試這個小技巧,體會「一次定義,處處生效」的爽快感。