許多初學者困惑:#define PI 3.1415 明明是「字符串替換」,為什麼 area = PI * r * r; 能正常進行浮點運算?本文從預處理器原理、Token 化、編譯器前端流程三個層面徹底釐清。
// 原始碼
#define PI 3.1415
double area = PI * r * r;
// 預處理後 (gcc -E 查看)
double area = 3.1415 * r * r;
關鍵認知:預處理器不理解 C 語法,只做 Token 級別的查找替換。替換後的 3.1415 是合法的 浮點數字面常數,編譯器前端(Lexer/Parser)將其識別為 double 型別常量,進而參與後續語義分析與代碼生成。
源碼 (.c)
│
▼
預處理器 (cpp) ──► 展開巨集、處理 #include、條件編譯
│
▼
詞法分析 (Lexer) ──► 識別 Token: 3.1415 (FLOAT_CONSTANT), *, r, *, r
│
▼
語法分析 (Parser) ──► 建構 AST: BinaryOp(*, BinaryOp(*, 3.1415, r), r)
│
▼
語義分析 ──► 型別推導: 3.1415 → double, r → double, 結果 → double
│
▼
代碼生成 ──► 指令: mulsd, mulsd (x86-64 SSE2)
| 特性 | 巨集 #define PI 3.1415 | 常數變數 const double PI = 3.1415; |
|---|---|---|
| 儲存空間 | 無(純文本替換) | 有(唯讀資料段,可能被優化掉) |
| 型別檢查 | 無(由編譯器在使用處推導) | 有(編譯期強型別) |
| 作用域 | 從定義處到檔案結尾 / #undef | 遵循區塊/檔案作用域規則 |
| 除錯可見性 | GDB 中看不到符號 | 可在 GDB 中 print PI |
| 位址操作 | &PI 錯誤 | &PI 合法 |
| 現代 C 推薦 | 僅用於編譯期配置、條件編譯 | 首選:constexpr (C23) / const / enum |
#define SQUARE(x) x * x
int a = SQUARE(3 + 2); // 展開: 3 + 2 * 3 + 2 = 3 + 6 + 2 = 11 ❌
// 正確:
#define SQUARE(x) ((x) * (x))
// 或現代寫法:
static inline int square(int x) { return x * x; }
#define STR(x) #x // 字符串化
#define CONCAT(a, b) a##b // Token 串聯
STR(hello) → "hello"
CONCAT(var, 1) → var1
巨集不是變數,是編譯器看到源碼前的「文本整形手術」。PI * r * r 能運算,是因為替換後的 3.1415 恰好是合法的浮點數 Token,被後續編譯階段正常處理。理解「預處理器無語法感知、編譯器才負責語義」這一分工,是避開巨集陷阱、寫出可維護代碼的關鍵認知基石。