在 C/C++ 前置處理器中,# 運算子 (Stringizing Operator) 可將巨集參數不經展開直接轉換為字串字面值,並自動加上雙引號與必要的逸出處理。
#define SymbolToStr(s) #s
// 展開範例
SymbolToStr(HelloWorld) → "HelloWorld"
SymbolToStr(x + y) → "x + y"
SymbolToStr("AlreadyStr") → "\"AlreadyStr\""
enum LogLevel { DEBUG, INFO, WARN, ERROR, FATAL };
#define LogLevelToStr(level) #level
void Log(LogLevel level, const char* msg)
{
printf("[%s] %s\n", LogLevelToStr(level), msg);
// 輸出: [DEBUG] 訊息內容
}
// 更進階:支援任意符號 (變數名、函數名、巨集名)
#define LOG_VAR(var) printf(#var " = %d\n", var)
int count = 42;
LOG_VAR(count); // 輸出: count = 42
#define DEBUG_TRACE(fmt, ...) \
do { \
fprintf(stderr, "[TRACE] %s:%d %s() " fmt "\n", \
__FILE__, __LINE__, __func__, ##__VA_ARGS__); \
} while(0)
#define DEBUG_VAR(var) \
do { \
fprintf(stderr, "[VAR] %s:%d %s = %d\n", \
__FILE__, __LINE__, #var, var); \
} while(0)
// 使用範例
void ProcessData(int id, int retry)
{
DEBUG_TRACE("開始處理 ID=%d", id);
int timeout = 30;
DEBUG_VAR(timeout);
if (retry > 3) DEBUG_TRACE("重試次數過多: %d", retry);
}
// 輸出:
// [TRACE] process.c:15 ProcessData() 開始處理 ID=1001
// [VAR] process.c:17 timeout = 30
// [TRACE] process.c:19 ProcessData() 重試次數過多: 5
#define ASSERT_EXPR(expr) \
do { \
if (!(expr)) { \
fprintf(stderr, "[ASSERT FAILED] %s:%d: %s\n", \
__FILE__, __LINE__, #expr); \
abort(); \
} \
} while(0)
void TestStack()
{
Stack s; s.Push(1); s.Push(2);
ASSERT_EXPR(s.Size() == 2); // 通過
ASSERT_EXPR(s.Top() == 2); // 通過
s.Pop();
ASSERT_EXPR(s.Size() == 1); // 通過
ASSERT_EXPR(s.Top() == 1); // 通過
// ASSERT_EXPR(s.Top() == 2); // 失敗輸出: [ASSERT FAILED] test.c:45: s.Top() == 2
}
// 產生 Get/Set 函數名稱
#define DECLARE_PROPERTY(type, name) \
type Get##name() const { return m_##name; } \
void Set##name(type val) { m_##name = val; } \
static const char* NameOf##name() { return #name; }
class Person
{
DECLARE_PROPERTY(std::string, Name)
DECLARE_PROPERTY(int, Age)
private:
std::string m_Name;
int m_Age;
};
// 展開後:
// std::string GetName() const { return m_Name; }
// void SetName(std::string val) { m_Name = val; }
// static const char* NameOfName() { return "Name"; }
// ... Age 同理
// 狀態碼定義
#define STATUS_CODES \
X(SUCCESS, 0) \
X(INVALID_PARAM, -1) \
X(OUT_OF_MEMORY, -2) \
X(IO_ERROR, -3)
// 自動產生枚舉
#define X(name, val) name = val,
enum StatusCode { STATUS_CODES };
#undef X
// 自動產生字串化對照表
#define X(name, val) { name, #name },
const std::pair StatusCodeStr[] = { STATUS_CODES };
#undef X
// 查找函數
const char* StatusToStr(StatusCode code)
{
for (auto& p : StatusCodeStr)
if (p.first == code) return p.second;
return "UNKNOWN";
}
// 使用
StatusToStr(SUCCESS); // "SUCCESS"
StatusToStr(INVALID_PARAM); // "INVALID_PARAM"
| 限制 | 說明 | 解決方案 |
|---|---|---|
| 不支援參數預先展開 | #define A 100; #A → "A" 而非 "100" | 雙層巨集: #define STR(s) #s; #define XSTR(s) STR(s) → XSTR(A) → "100" | 字串字面值會被雙重引號包裹 | #"hello" → "\"hello\"" | 預期行為,若需原始字串請勿對字面值使用 # |
| 巨集參數空白字元保留 | # a + b → " a + b " | 前置處理器會壓縮前導/尾隨空白,但保留中間空白 |
| 無法處理多行參數 | 參數跨行需用 \ 續行 | 設計時避免跨行巨集參數 |
// C++20: source_location 捕獲檔案/行號/函數
#include
#include
void ModernLog(std::string_view msg, std::source_location loc = std::source_location::current())
{
std::cerr << std::format("[{}:{} {}] {}\n", loc.file_name(), loc.line(), loc.function_name(), msg);
}
// C++23: std::to_underlying, std::format 增強
// 反射提案 (P2996) 未來可直接取得列舉名稱字串,無需巨集
# 字串化運算子是 C/C++ 前置處理器的「瑞士刀」之一,配合 ## 連接運算子、__FILE__/__LINE__/__func__ 預定義巨集,能以極低成本構建強大的日誌、追蹤、斷言、序列化、程式碼生成基礎設施。雖然現代 C++ 提供 source_location、format、未來反射等型別安全替代方案,但在相容 C、嵌入式、跨平台、零依賴、編譯期計算等場景,巨集字串化技術仍具不可替代的實用價值。