[C 的那些眉角]#define vs const vs enum — 定義常數,哪種才對?

在嵌入式開發裡,定義常數這件事做了幾萬次,但我敢說大多數人都沒有認真想過「這三種到底有什麼差別」。

反正 #define 能跑、const 也能用,切換狀態就 enum,好像各有各的場,不去想也沒事。

直到某次 code review,被人問了一句:「這裡為什麼用 #define 而不是 const?」

我愣了一下。說真的,我沒有理由,就只是習慣。


先說結論

不想看完可以直接記這個:

情境 建議
硬體暫存器位址、bit mask #define
有型別的常數值 const
一組互斥的狀態 / 選項 enum
條件編譯、平台切換 #define
陣列長度(C99 以後) enumconst 均可,但 #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 標準),不能用在 switchcase

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_tuint32_tenum 做不到
  • 兩個 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 到底,要好維護一些。


💬 你在專案裡偏好用哪種?有沒有遇過因為選錯而踩坑的情況?歡迎留言說說你的狀況。


發佈留言