[C 的那些眉角]型別轉換的隱式陷阱 — 你沒有要轉,但它轉了

有一次我在 review 同事的 code,看到一行:

if (len - sizeof(header) > 0) {

我當下沒說什麼,因為看起來很正常。結果那週 QA 回報說某個封包長度剛好等於 header size 的時候,系統會亂跑。

追進去才發現:lenintsizeof(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。而 0xFFint literal,值是 255。-1 != 255,不進來。

如果想比較 byte 值,用 uint8_tunsigned 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_tuint32_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 混用的表達式,就會多停一秒。養成習慣之後,其實也不麻煩。


💬 你有遇過類似的隱式轉換雷嗎?或者有什麼我漏掉的常見陷阱?歡迎留言聊聊。

發佈留言