#define 可以說是 C 語言裡最早學到的東西之一。
#define MAX_SIZE 256
#define PI 3.14159
看起來很無害,對吧?
但用久了才發現,#define 其實是個很容易出事的工具。
它不是變數、不是函式、不遵守 scope、不做型別檢查,
就是單純的文字替換。
而「單純的文字替換」,在某些情況下會產生你完全沒預期到的結果。
這篇記錄幾個我自己踩過、或在 code review 看過的坑。
坑 #1:Macro 運算式沒加括號
這是最經典的,但還是一直有人踩。
#define SQUARE(x) x * x
看起來沒問題,但:
int result = SQUARE(3 + 1);
展開之後變成:
int result = 3 + 1 * 3 + 1; // = 7,不是 16
因為 preprocessor 只是做文字替換,不管運算子優先順序。
正確寫法是每個參數、整個運算式都加括號:
#define SQUARE(x) ((x) * (x))
展開後:
int result = ((3 + 1) * (3 + 1)); // = 16 ✅
原則:Macro 的參數和整體,全部都加括號。
坑 #2:參數有副作用,被展開兩次
括號加好了,但還有另一個問題:
#define SQUARE(x) ((x) * (x))
int i = 3;
int result = SQUARE(i++);
展開後:
int result = ((i++) * (i++));
i++ 被執行了兩次。
結果是 undefined behavior,不同編譯器、不同優化等級,
跑出來的結果可能都不一樣。
這種 bug 很難找,因為你根本不會去懷疑一個「看起來像函式」的東西
會把參數展開兩次。
原則:有副作用的運算式(i++、函式呼叫)不要傳進 Macro。
坑 #3:多行 Macro 沒用 do { } while(0)
有時候 Macro 會包含多行程式碼:
#define RESET_DEVICE() \
gpio_set_low(RESET_PIN); \
delay_ms(10); \
gpio_set_high(RESET_PIN);
這樣寫在大部分情況下沒問題,但遇到 if/else 就爆了:
if (error)
RESET_DEVICE();
else
do_something();
展開後:
if (error)
gpio_set_low(RESET_PIN);
delay_ms(10); // 這行永遠執行!
gpio_set_high(RESET_PIN); // 這行也是!
else // 這個 else 找不到對應的 if,編譯錯誤
do_something();
正確寫法是用 do { } while(0) 包起來:
#define RESET_DEVICE() \
do { \
gpio_set_low(RESET_PIN); \
delay_ms(10); \
gpio_set_high(RESET_PIN); \
} while(0)
這樣不管在什麼情況下使用,行為都是正確的,
而且結尾的分號也不會出問題。
第一次看到這個寫法的時候覺得很奇怪,
後來才理解這是 C 語言的慣用模式。
坑 #4:Macro 名稱跟變數撞名
#define 不管 scope,整個 translation unit 都有效。
#define SIZE 10
void some_function(void) {
int SIZE = get_buffer_size(); // 編譯錯誤或奇怪的行為
...
}
preprocessor 會把 SIZE 直接換成 10,
所以上面那行變成 int 10 = get_buffer_size();,
直接編譯錯誤。
這個還算容易發現。更難發現的是:
#define MAX 100
// 某個第三方 library 的 header
typedef struct {
int max; // 這個 max 沒事
int MAX; // 這個會被展開成 int 100,編譯錯誤
} Config;
include 順序不同,結果就不同,很難 debug。
原則:Macro 名稱全大寫加底線,減少撞名機會。
原則:能用 const 或 enum 的地方,就不要用 #define。
什麼時候該用 const 取代 #define
定義常數的時候,const 比 #define 好很多:
// ❌ #define 版本
#define MAX_RETRY_COUNT 3
#define TIMEOUT_MS 5000
// ✅ const 版本
const int MAX_RETRY_COUNT = 3;
const uint32_t TIMEOUT_MS = 5000;
const 的好處:
- 有型別,編譯器會做型別檢查
- 有 scope,可以限制在函式或檔案內
- debugger 看得到,
#define在 debug 的時候是看不到的 - 不會有文字替換的副作用
唯一 #define 必須用的情況是:需要在編譯期做條件判斷的時候。
// 這種情況只能用 #define
#if defined(BOARD_VERSION) && BOARD_VERSION >= 2
// 新版硬體的初始化
#else
// 舊版硬體的初始化
#endif
什麼時候該用 enum 取代 #define
一組相關的常數,用 enum 比一堆 #define 好:
// ❌ 一堆 #define
#define STATE_IDLE 0
#define STATE_CONNECTING 1
#define STATE_CONNECTED 2
#define STATE_ERROR 3
// ✅ enum
typedef enum {
STATE_IDLE = 0,
STATE_CONNECTING,
STATE_CONNECTED,
STATE_ERROR
} DeviceState;
enum 的好處:
- 有型別,
switch的時候編譯器可以警告你漏掉某個 case - debugger 可以顯示名稱,不只是數字
- 語意更清楚,這些值是一組相關的狀態
那 Macro 函式呢?
有些情況下,Macro 函式還是有它的用途:
// 取最大值,支援任何型別
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
但如果編譯器支援 C99 以上,可以考慮用 static inline 函式取代:
static inline int max_int(int a, int b) {
return (a > b) ? a : b;
}
static inline 的好處:
- 有型別檢查
- 沒有副作用問題
- debugger 可以進去看
- 大部分情況下效能跟 Macro 一樣(編譯器會 inline)
壞處是不支援多型別,每種型別要寫一個版本。
這時候 Macro 還是有它的價值。
說實話
我現在的習慣是:
能用 const 就用 const,能用 enum 就用 enum,能用 static inline 就用 static inline。#define 留給真的需要 preprocessor 的地方。
但現實是,很多嵌入式專案的舊程式碼裡,
#define 滿天飛,也不可能全部重構。
所以至少要知道這些坑在哪裡,看到有副作用的 Macro 參數,
或是多行 Macro 沒有 do { } while(0),
要有那個直覺「這裡可能會出事」。
Checklist
- [ ] Macro 的參數和整體運算式都有加括號
- [ ] 沒有把有副作用的運算式(
i++、函式呼叫)傳進 Macro - [ ] 多行 Macro 有用
do { } while(0)包起來 - [ ] Macro 名稱全大寫加底線,避免撞名
- [ ] 定義常數優先用
const,一組相關常數用enum - [ ] 簡單的 Macro 函式考慮改用
static inline