有一次我在 review 同事的 code,看到一行:
if (len - sizeof(header) > 0) {
我當下沒說什麼,因為看起來很正常。結果那週 QA 回報說某個封包長度剛好等於 header size 的時候,系統會亂跑。
追進去才發現:len 是 int,sizeof(header) 是 size_t,也就是 unsigned。當 len == sizeof(header) 的時候,相減結果是 0,但如果 len < sizeof(header),你以為是負數,其實是一個很大的正整數。然後 > 0 就是 true。
然後就囧了。
為什麼這麼陰?
C 的 Usual Arithmetic Conversions 規則:當 signed 和 unsigned 做運算,signed 會被轉成 unsigned。
這件事編譯器預設不一定警告你,程式碼看起來也完全合理,只有在邊界條件才會爆。
而韌體很常在邊界條件爆。
常見的隱式轉換地雷
1. signed 和 unsigned 混算
int len = 10;
size_t header_size = 20;
if (len - header_size > 0) { // 永遠是 true!
// 你以為這裡不會進來
process_payload(...);
}
len - header_size 實際上是 (size_t)10 - (size_t)20,結果是 0xFFFFFFFFFFFFFFF6,不是 -10。
修法很簡單,但得先知道有這個洞:
if (len > (int)header_size) { // 明確 cast,意圖清楚
process_payload(...);
}
2. char 做比較的時候
這個更經典。
char c = 0xFF;
if (c == 0xFF) {
// 你以為會進來
}
char 在大部分平台是 signed,所以 0xFF 會被解讀成 -1。而 0xFF 是 int literal,值是 255。-1 != 255,不進來。
如果想比較 byte 值,用 uint8_t 或 unsigned char,別用 char。
uint8_t c = 0xFF;
if (c == 0xFF) { // OK,都是 unsigned
// 這次進來了
}
3. 函式回傳值的轉換
int8_t get_offset(void) {
return -5;
}
uint16_t result = 100 + get_offset(); // 結果是多少?
get_offset() 回傳 int8_t,但做加法時會 promote 到 int,所以 -5 還是 -5,然後 100 + (-5) = 95,再存到 uint16_t,結果是 95。這個沒錯。
但如果你這樣寫:
uint16_t base = 100;
int8_t offset = -5;
uint16_t result = base + offset; // base 是 unsigned,offset 被轉成 unsigned
offset 會被轉成 uint16_t,變成 65531,然後 100 + 65531 = 65631,再截斷成 uint16_t……這就爛了。
4. 移位運算的隱雷
int x = -1;
unsigned int result = x >> 1; // 這是 implementation-defined
對 signed 做右移,C 標準說是 implementation-defined。大部分平台是算術右移(補 1),但不是保證。而且對負數左移是 undefined behavior。
移位最好只對 unsigned 做:
uint32_t x = 0xFFFFFFFF;
uint32_t result = x >> 1; // 這個行為是明確的
開 warning 其實就抓得到
-Wsign-compare 和 -Wconversion 可以抓大部分這類問題:
CFLAGS += -Wall -Wextra -Wsign-compare -Wconversion
但 -Wconversion 很吵,很多人嫌煩就關掉了。我的習慣是新專案開著忍一忍,把真正有問題的改掉,然後留著。legacy code 就……算了,看情況。
型別規範的懶人心法
在嵌入式專案裡,我習慣幾個原則:
- buffer 大小、長度類的變數:用
size_t或uint32_t,不要用int - 可能有負值的 offset:用
int32_t,意圖明確 - 比較長度前先確認同號:不要讓 signed 和 unsigned 直接相減再比
- byte 值:永遠用
uint8_t,不要用char - 移位:只對 unsigned 做
這不是什麼高深原則,就是吃過幾次虧之後慢慢養成的習慣。
還有一個坑我到現在還很小心
int 在不同平台大小不一樣。8-bit MCU 上 int 可能是 16-bit,x86 是 32-bit,這個在移植 code 的時候會出問題。
如果你的 code 有可能跑在不同架構上,固定寬度型別(int8_t, uint16_t, int32_t...)是唯一的選擇。
<stdint.h> 從 C99 就有了,沒有理由不用。
小結
隱式型別轉換不是什麼冷僻知識,但它的問題是「不報錯、不警告、大部分時候還跑得對」,只有在某個特定邊界條件下才發作。
這種 bug 最難追,因為你根本不知道要懷疑它。
所以我現在看到 signed 和 unsigned 混用的表達式,就會多停一秒。養成習慣之後,其實也不麻煩。
💬 你有遇過類似的隱式轉換雷嗎?或者有什麼我漏掉的常見陷阱?歡迎留言聊聊。