在嵌入式開發裡,定義常數這件事做了幾萬次,但我敢說大多數人都沒有認真想過「這三種到底有什麼差別」。
反正 #define 能跑、const 也能用,切換狀態就 enum,好像各有各的場,不去想也沒事。
直到某次 code review,被人問了一句:「這裡為什麼用 #define 而不是 const?」
我愣了一下。說真的,我沒有理由,就只是習慣。
先說結論
不想看完可以直接記這個:
| 情境 | 建議 |
|---|---|
| 硬體暫存器位址、bit mask | #define |
| 有型別的常數值 | const |
| 一組互斥的狀態 / 選項 | enum |
| 條件編譯、平台切換 | #define |
| 陣列長度(C99 以後) | enum 或 const 均可,但 #define 最保險 |
但只知道結論沒什麼用,知道背後的理由才不會在邊緣情況翻車。
define:強大但粗暴
#define 是純文字替換,在預處理階段就展開,編譯器根本看不到它。
#define MAX_RETRY 5
#define TIMEOUT_MS 1000
#define BIT(n) (1U << (n))
好用的地方:
- 沒有型別、沒有 scope,任何地方都能用
- 條件編譯必須用它
- 硬體位址、bit 操作一般都用這個,直覺
容易出事的地方:
#define SQUARE(x) x * x
int result = SQUARE(3 + 1); // 展開成 3 + 1 * 3 + 1 = 7,不是 16
這個例子很老套,但我真的在 production code 裡看過類似的坑。巨集沒有括號保護,展開後運算順序亂掉。
另一個問題是偵錯的時候很痛苦。#define 不佔記憶體、不在 symbol table 裡,GDB 直接說它不認識這個符號。
還有:沒有型別檢查。
#define MAX_SIZE -1 // 某天有人手滑
uint8_t buf[MAX_SIZE]; // 展開成超大陣列,編譯器不一定會告訴你
const:有型別的真正常數
const int max_retry = 5;
const uint32_t timeout_ms = 1000U;
const 是 C 的型別系統的一部分,編譯器看得到它,有完整的型別檢查。
真正的優勢:
- 型別明確,傳進函式會做型別檢查
- 有 scope,可以限制在 file 內或 function 內
- GDB 可以看到,偵錯方便很多
- 可以取位址(
#define不行)
嵌入式的坑:
C 的 const 不像 C++ 那麼乾淨。在 C 裡面,const 的值在某些情況下不算「編譯期常數」,不能用來宣告陣列大小(C89 標準),不能用在 switch 的 case。
const int SIZE = 10;
int arr[SIZE]; // C89 不合法,C99 VLA,C++ 沒問題
如果你的工具鏈還在吃 C89(某些 MCU 的 IDE 預設),要注意。
另一個常被忽略的點:const 在 C 裡面不保證放在 ROM,取決於編譯器和連結器設定。如果你在意「這個常數一定要在 flash 裡」,要看你的 .ld 或明確加 __flash 之類的 attribute。
enum:被低估的好東西
大部分人用 enum 只拿來做狀態機:
typedef enum {
STATE_IDLE,
STATE_RUNNING,
STATE_ERROR
} system_state_t;
但 enum 還有一個很實用但少人用的功能——當整數常數用:
typedef enum {
BUFFER_SIZE = 256,
HEADER_LEN = 4,
MAX_PAYLOAD = BUFFER_SIZE - HEADER_LEN
} protocol_config_t;
這樣的好處是:enum 的值一定是編譯期常數,可以用在陣列宣告、switch case,不會有 const 的那些限制。
不好的地方:
- 底層型別是
int,跨平台可能有問題(某些嵌入式平台int是 16-bit) - 如果你需要精確的
uint8_t或uint32_t,enum做不到 - 兩個
enum之間直接做算術,編譯器通常不會報錯,但語意上不清楚
我實際怎麼用
老實說,這三種我都在用,只是場景不一樣。
硬體相關的東西,還是用 #define:
#define UART0_BASE 0x40004400
#define REG_CR1 (UART0_BASE + 0x00)
#define CR1_UE_BIT BIT(0)
#define CR1_RE_BIT BIT(2)
這是慣例,大家都這樣寫,寫 const 反而奇怪。
邏輯層的配置參數,用 const:
const uint32_t kConnectTimeoutMs = 5000U;
const uint8_t kMaxRetryCount = 3U;
有型別、有 scope,出事的時候 GDB 找得到。
狀態、模式、選項,用 enum:
typedef enum {
CONN_MODE_STATION,
CONN_MODE_AP,
CONN_MODE_MIXED
} wifi_conn_mode_t;
傳進函式的時候,接收端一眼就知道這個參數是什麼意思,比傳一個 int 好讀太多了。
一個讓我改習慣的真實案例
之前維護一個通訊模組的 firmware,有個地方定義了封包最大長度:
#define MAX_PKT_LEN 128
後來需求改了,改成 256。改完之後 build 出去,某個地方掛了,追了快一個小時才發現:
uint8_t buf[MAX_PKT_LEN]; // 這裡有
uint8_t tmp[MAX_PKT_LEN/2]; // 這裡有
// 還有一個地方:
uint8_t rxbuf[128]; // 直接 hardcode 的,沒用 #define
那個 rxbuf[128] 沒有跟著改。
#define 本身沒錯,是當時沒有統一用它。如果那個地方用的是 const uint8_t kMaxPktLen = 128,至少搜 symbol 的時候更容易找出所有使用點。
這之後我對「這種常數要讓 compiler 幫我做型別檢查」這件事比較在意了。
C++ 的情況不一樣
如果你在寫 C++,constexpr 是更好的選擇:
constexpr int kMaxRetry = 5;
constexpr uint32_t kTimeoutMs = 1000U;
constexpr 是真正的編譯期常數,型別安全、有 scope、可以用在 template 裡,比 C 的 const 更可靠。嵌入式如果有機會用 C++11 以上,應該多用 constexpr。
沒有唯一解
這三種各有各的位置,硬要說「只用哪一種」是在裝。
我的習慣是:硬體底層 → #define,配置參數 → const,狀態/選項 → enum。這樣翻程式碼的時候,光看用的是哪種,大概就知道這個常數屬於哪個層次的東西。
至少比一整個專案都 #define 到底,要好維護一些。
💬 你在專案裡偏好用哪種?有沒有遇過因為選錯而踩坑的情況?歡迎留言說說你的狀況。