[C 的那些眉角]參數傳遞的眉角 — `const` 用對了嗎?

const 這個關鍵字,我用了很久都只會這樣寫:

const int MAX_SIZE = 256;

然後在函式參數上偶爾加一下,
覺得「這樣比較專業」,但其實不太確定為什麼要加。

直到有一次,同事在 code review 留言:

「這個參數應該加 const,你這樣寫呼叫端不知道你會不會改它。」

我才開始認真研究 const 在參數傳遞上到底是什麼意思。

結果發現,const 跟指標放在一起,
光是位置不同,意思就完全不一樣。

閱讀全文

[C 的那些眉角]回傳值不要亂丟 — 錯誤處理的設計

剛開始寫 C 的時候,我的錯誤處理大概是這樣:

void init_device(void) {
    i2c_init();
    sensor_init();
    uart_init();
    // 完成,應該沒問題吧?
}

回傳 void,裡面每個函式的回傳值都不管。

反正在開發板上跑都正常,就這樣出貨了。

然後客戶回報說裝置偶爾會初始化失敗,
但 log 完全看不出來哪個步驟出問題,
因為根本沒有任何錯誤處理。

那次之後,我開始認真思考:錯誤處理不是可選的,是必須的。


最常見的壞習慣

1. 完全不處理回傳值

閱讀全文

[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 的那些眉角]#define 的陷阱 — 沒用好會掉進坑

#define 可以說是 C 語言裡最早學到的東西之一。

#define MAX_SIZE 256
#define PI 3.14159

看起來很無害,對吧?

但用久了才發現,#define 其實是個很容易出事的工具。
它不是變數、不是函式、不遵守 scope、不做型別檢查,
就是單純的文字替換。

而「單純的文字替換」,在某些情況下會產生你完全沒預期到的結果。

這篇記錄幾個我自己踩過、或在 code review 看過的坑。

閱讀全文

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

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

i++;  // i 加 1

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

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

很直接,但他說得對。

閱讀全文

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

前言

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

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

那個人是我自己。

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


壞命名長什麼樣子

閱讀全文

那些年我踩過的 Memory Leak

做嵌入式 Linux 開發那幾年,Memory Leak 大概是我遇過最多次、最難抓、最容易被忽略的問題。

桌面應用程式記憶體洩漏,頂多讓程式跑慢、被 OS 回收。但在嵌入式系統上,RAM 本來就不多,一旦洩漏,輕則效能下降,重則系統直接 OOM crash,而且往往要跑好幾個小時才會發作

這篇文章整理我這幾年踩過的幾個經典 Memory Leak 案例,每個都有點不一樣,希望能幫你少走一些彎路。

⚠️ 程式碼為簡化示意版本,重點在於呈現問題模式


案例一:最經典的忘記 free 🐣

閱讀全文

[C 語言]為什麼sprintf要改用 snprintf?一次搞懂 C 語言的緩衝區安全

還記得第一次被 code reviewer 退回程式碼的心情嗎?當時我只是用了一個看似平凡無奇的 sprintf 函數,卻被標註為「嚴重安全問題」。當下我心想:「不就是格式化字串嗎?有這麼嚴重?」直到我深入了解後才發現,這個小小的函數選擇,可能是駭客入侵系統的大門。今天,讓我們一起來探討為什麼 sprintf 如此危險,以及為什麼所有 C 語言開發者都應該改用 snprintf


一、sprintf vs snprintf:看似相同,實則天差地別

閱讀全文

[C 語言]註解大解密:/* */ vs // 該怎麼選?

在學習 C 語言的過程中,你是否曾經困惑過:為什麼有些程式碼用 // 註解,有些卻用 /* */?哪一種比較好?這個看似簡單的問題,其實藏著不少學問。今天,讓我們一起深入探討這兩種註解方式的優缺點,幫助你在不同情境下做出最佳選擇!


🔍 兩種註解方式的歷史背景

在開始比較之前,先了解一下歷史:/* */ 是 C 語言最早期(C89/C90 標準)就存在的註解方式,而 // 則是從 C++ 借鑑過來,直到 C99 標準才正式納入 C 語言。這個歷史差異也解釋了為什麼在某些舊系統或嵌入式開發中,/* */ 仍然佔據主導地位。

📊 詳細比較:優缺點分析

閱讀全文