[C 的那些眉角]switch 記得加 default — 防禦未來的自己

有一種 bug,不是今天的你造成的,
是三個月後的你造成的。

你現在寫了一個 switch
處理三種狀態,邏輯完全正確,
測試也都過了。

三個月後,需求改了,
新增了第四種狀態。
你在 enum 裡加了一個值,
但忘記去更新那個 switch

編譯過了,沒有警告,
程式跑起來,遇到第四種狀態的時候,
switch 什麼都不做,
靜靜地繼續往下執行。

症狀可能是某個功能沒有反應,
可能是某個變數沒有被更新,
可能要跑很久才會觸發那個狀態,
然後你花了半天才找到原因。

default 加一行,可以讓這種問題立刻現形。

閱讀全文

[C 的那些眉角]整數溢位比你想的更常見

有一個 bug,我在 code review 的時候無意間看到,
當下覺得「這不會有問題吧」,
但仔細算了一下,發現真的會出問題。

uint16_t a = 60000;
uint16_t b = 10000;
uint16_t sum = a + b; // 你覺得 sum 是多少?

答案不是 70000。

uint16_t 最大值是 65535,
70000 超過了,發生溢位,
sum 實際上是 70000 - 65536 = 4464

程式繼續跑,用著這個錯誤的值,
後續的計算全部都錯了,
但不會 crash,不會有任何警告。

這就是整數溢位的恐怖之處。

閱讀全文

[C 的那些眉角]陣列邊界檢查 — 緩衝區溢位的根源

緩衝區溢位(Buffer Overflow)是 C 語言最惡名昭彰的問題之一。

它不只是會讓程式 crash,
在某些情況下,它是駭客攻擊的入口。
歷史上很多嚴重的安全漏洞,
根源都是一個沒有做邊界檢查的陣列存取。

但在嵌入式開發,我更常遇到的不是安全問題,
而是「程式跑著跑著,某個全域變數的值莫名其妙被改掉」,
或是「UART 收到一個比預期長的封包,程式就 crash 了」。

追到最後,都是同一件事:
有人寫到了陣列邊界以外的地方。

閱讀全文

[C 的那些眉角]malloc 之後一定要檢查 — 記憶體配置的防禦性寫法

平常在嵌入式系統上用 malloc
寫完之後覺得很爽,動態配置記憶體,好像很厲害。

uint8_t *buf = malloc(1024);
memset(buf, 0, 1024);
// 開始用 buf...

有一次朋友看了一眼問我:「malloc 失敗怎麼辦?」

我說:「會失敗嗎?記憶體應該夠吧?」

他說:「嵌入式的 heap 就那麼大,你確定嗎?」

我 ........ 當然不是很確定。

閱讀全文

[C 的那些眉角]指標用完要歸零 — 懸空指標的恐怖故事

有一種 bug,我只要想到就頭皮發麻。

明明程式跑得好好的,突然在某個完全不相關的地方 crash,
或是資料莫名其妙被改掉,
或是在開發機上完全正常,到了產品上偶爾出問題。

很多時候,追到最後都是同一個兇手:

懸空指標(Dangling Pointer)。

閱讀全文

[C 的那些眉角]函式介面設計 — 呼叫時不易搞錯

話說有一種 bug 很特別。

不是邏輯寫錯,不是演算法有問題,
而是呼叫端用錯了你的函式

參數順序傳反了、單位搞錯了、忘記先初始化就呼叫、
buffer 大小傳錯了……

這種 bug 有時候很難發現,因為程式可能還是跑起來,
只是結果不對,或是偶爾 crash。

我以前遇到這種情況,第一反應是「呼叫端的問題,他用錯了」。

但後來慢慢體會到:如果很多人都用錯,問題通常在介面設計,不在使用者。

閱讀全文

[C 的那些眉角]一個函式只做一件事 — 聽起來簡單但很難

「一個函式只做一件事。」

這句話我很早就聽過,覺得自己懂了,然後繼續寫出這種東西:

int process_sensor_data(void) {
    // 讀取感測器
    uint8_t raw[8];
    i2c_read(SENSOR_ADDR, raw, sizeof(raw));

    // 解析資料
    int16_t temp = (raw[0] << 8) | raw[1];
    int16_t humidity = (raw[2] << 8) | raw[3];

    // 換算單位
    float temp_c = temp / 100.0f;
    float humi_pct = humidity / 100.0f;

    // 檢查是否超過閾值
    if (temp_c > 85.0f || humi_pct > 95.0f) {
        trigger_alarm();
    }

    // 存到全域變數
    g_temperature = temp_c;
    g_humidity = humi_pct;

    // 發送到 server
    mqtt_publish("sensor/data", temp_c, humi_pct);

    return 0;
}

這個函式做了幾件事?

讀取、解析、換算、判斷、存值、發送。六件事。

當時覺得「這樣寫很方便,一個函式搞定所有事情」。
直到需要修改的時候才發現,牽一髮動全身,
改個閾值要找半天,加個錯誤處理不知道要加在哪裡,
單元測試根本不知道從哪裡下手。

閱讀全文

[C 的那些眉角]永遠初始化你的變數 — 那個神秘的值

剛學 C 的時候,老師說過:「變數要記得初始化。」

我點點頭,然後還是繼續寫:

int count;
char buf[64];
int *ptr;

反正當下程式跑起來好像也沒問題。

直到有一天,發現程式出現一個「偶發性」的奇怪行為。
有時候正常,有時候不正常,完全沒有規律。

閱讀全文

[C 的那些眉角]註解要寫「為什麼」,不是「做什麼」

記得很久以前,我寫註解的方式大概類似這樣:

i++;  // i 加 1

現在想起來,覺得很好笑。
當時覺得「有寫註解」這件事本身就是好習慣,
至於寫了什麼⋯⋯好像不太重要?

後來進公司,第一次被 senior 在 code review 上留言:
「這個註解跟沒寫一樣。」

很直接,但他說得對。

閱讀全文

[C 的那些眉角]命名不是小事 — 讓程式碼自己說話

前言

剛開始寫 C 的時候,我覺得命名這件事不重要。

反正編譯器不在乎變數叫 a 還是 temperature,程式跑起來結果一樣。
直到有一天,我打開三個月前自己寫的程式碼,盯著一個叫 tmp2 的變數看了五分鐘,
完全不知道它是幹嘛的。

那個人是我自己。

從那之後,我開始認真對待命名這件事。這篇是我的一些整理,
不是什麼業界標準,就是我覺得「這樣寫,之後比較不會想罵自己」的習慣。


壞命名長什麼樣子

閱讀全文