const 這個關鍵字,我用了很久都只會這樣寫:
const int MAX_SIZE = 256;
然後在函式參數上偶爾加一下,
覺得「這樣比較專業」,但其實不太確定為什麼要加。
直到有一次,同事在 code review 留言:
「這個參數應該加
const,你這樣寫呼叫端不知道你會不會改它。」
我才開始認真研究 const 在參數傳遞上到底是什麼意思。
結果發現,const 跟指標放在一起,
光是位置不同,意思就完全不一樣。
先從基本的說起
傳值(Pass by Value)
void foo(int x) {
x = 100; // 只改了 local copy,呼叫端的變數不受影響
}
int a = 5;
foo(a);
// a 還是 5
傳值的時候,函式拿到的是一份拷貝。
改了不影響呼叫端,所以這種情況加不加 const 對呼叫端沒差:
void foo(const int x) {
x = 100; // 編譯錯誤,但呼叫端本來就不受影響
}
傳值加 const,只是告訴編譯器「這個 local copy 我不會改」,
對呼叫端沒有任何保證的意義。
實務上,傳值的參數我通常不加 const,沒什麼必要。
傳指標(Pass by Pointer)
這裡才是 const 真正有意義的地方。
傳指標的時候,函式可以透過指標修改呼叫端的資料,
const 可以限制這件事。
但問題是,const 跟指標放在一起有四種寫法,
每種意思都不一樣。
const 與指標的四種組合
我用一個例子來說明,假設有個 uint8_t *p:
組合一:const uint8_t *p(指向常數的指標)
void print_data(const uint8_t *p, size_t len) {
for (size_t i = 0; i < len; i++) {
printf("%02X ", p[i]); // 可以讀
}
p[0] = 0xFF; // ❌ 編譯錯誤,不能透過 p 修改資料
}
const 在 * 左邊,代表指標指向的內容是唯讀的。
函式承諾:「我只會讀這塊資料,不會改它。」
這是最常用的寫法,用在「只需要讀取資料」的參數上。
組合二:uint8_t * const p(常數指標)
void foo(uint8_t * const p, size_t len) {
p[0] = 0xFF; // ✅ 可以修改指標指向的內容
p = other_buf; // ❌ 編譯錯誤,不能改變指標本身
}
const 在 * 右邊,代表指標本身不能改變,
但指向的內容可以改。
這個在函式參數上比較少用,
因為指標是傳值進來的,就算改了也不影響呼叫端。
組合三:const uint8_t * const p(常數指標指向常數)
void foo(const uint8_t * const p, size_t len) {
p[0] = 0xFF; // ❌ 不能改內容
p = other_buf; // ❌ 不能改指標
}
兩個都不能改。
實務上比較少見,但如果你想表達「這個參數完全唯讀」,
這是最嚴格的寫法。
組合四:uint8_t *p(什麼都沒有)
void foo(uint8_t *p, size_t len) {
p[0] = 0xFF; // ✅ 可以改內容
p = other_buf; // ✅ 可以改指標(但不影響呼叫端)
}
沒有任何限制,函式可以任意修改指向的內容。
用一張表整理
| 寫法 | 能改指標本身 | 能改指向的內容 | 常用場景 |
|---|---|---|---|
const uint8_t *p |
✅ | ❌ | 唯讀參數(最常用) |
uint8_t * const p |
❌ | ✅ | 少用 |
const uint8_t * const p |
❌ | ❌ | 完全唯讀 |
uint8_t *p |
✅ | ✅ | 需要修改內容的參數 |
實際應用:什麼時候該加?
只讀資料,一定要加
// ❌ 沒加 const,呼叫端不知道你會不會改 buf
int uart_send(uint8_t *buf, size_t len);
// ✅ 加了 const,明確承諾不修改 buf
int uart_send(const uint8_t *buf, size_t len);
這個承諾很重要。
呼叫端可能傳進來一個 const 的資料:
const uint8_t greeting[] = "Hello";
uart_send(greeting, sizeof(greeting));
如果 uart_send 的參數沒有 const,
這行會產生編譯警告(甚至錯誤),
因為你把 const 的東西傳給一個「可能會改它」的函式。
需要修改內容,不要加
// 這個函式要把資料寫進 buf,所以不能加 const
int uart_receive(uint8_t *buf, size_t buf_size, uint32_t timeout_ms);
這裡不加 const 是正確的,
代表「我會把資料寫進這個 buffer」。
字串參數幾乎都應該加
// ❌
void log_message(char *msg);
// ✅
void log_message(const char *msg);
Log 函式只需要讀字串,不需要改它,
加 const 讓呼叫端可以直接傳字串常數進來:
log_message("System initialized"); // 字串常數,需要 const 才不會警告
一個常見的誤解
很多人以為加了 const 就代表「這個變數不會被改」,
但其實 const 可以被 cast 掉:
void evil_function(const uint8_t *p) {
uint8_t *writable = (uint8_t *)p; // cast 掉 const
writable[0] = 0xFF; // 改了!
}
這樣做是 undefined behavior,
但編譯器不一定會報錯。
所以 const 是一個介面上的承諾,
不是硬體層面的保護。
它的主要作用是:
- 讓編譯器幫你檢查有沒有意外修改
- 讓呼叫端知道這個函式的意圖
- 讓
const的資料可以安全地傳進去
嵌入式的特別情況:Flash 裡的常數
在嵌入式,const 還有一個實際的用途:
告訴編譯器這個資料可以放在 Flash(ROM)裡,不需要佔用 RAM。
// 這個陣列會放在 Flash,不佔 RAM
const uint8_t font_table[] = {
0x3E, 0x51, 0x49, 0x45, 0x3E, // '0'
0x00, 0x42, 0x7F, 0x40, 0x00, // '1'
// ...
};
在 RAM 很珍貴的 MCU 上(幾 KB 的 SRAM),
這個差別非常重要。
如果沒有加 const,編譯器可能會把這個陣列複製到 RAM,
白白浪費記憶體。
說實話
const 這件事,我覺得最難的不是記住四種組合,
而是養成習慣,每次寫函式參數的時候都問自己:
「這個指標,我需要修改它指向的內容嗎?」
不需要修改 → 加 const uint8_t *
需要修改 → 不加
就這樣,其他兩種組合在函式參數上很少用到,
先把這個基本的搞清楚就夠了。
我現在的習慣是,寫完函式之後,
回頭看每個指標參數,問自己有沒有漏加 const。
漏加不會讓程式跑不起來,
但會讓介面變得模糊,呼叫端不知道你的意圖。
這篇的 Checklist
- [ ] 只讀取、不修改的指標參數有加
const - [ ] 字串參數(
char *)有加const - [ ] 需要修改內容的指標參數沒有誤加
const - [ ] 嵌入式的大型常數陣列有加
const(放 Flash 省 RAM) - [ ] 沒有用 cast 把
const偷偷去掉 - [ ] 函式介面的
const使用一致,不是隨機加的