[C好習慣筆記]命名不是小事 — 讓程式碼自己說話

前言

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

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

那個人是我自己。

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


壞命名長什麼樣子

先看幾個真實世界常見的例子(我自己也寫過):

int d;           // 什麼 d?day?data?distance?
int tmp, tmp2;   // 好,那 tmp2 是 tmp 的什麼?
void process();  // 處理什麼?
int flag;        // 哪個 flag?代表什麼狀態?
int data[256];   // data 是什麼 data?

這些命名有個共同問題:你必須去看實作才能知道它是什麼

好的命名應該讓你光看名字就大概知道它的用途。


幾個我在用的原則

1. 名字要說出「它是什麼」,不是「它怎麼用」

// ❌ 描述用途,但不夠清楚
int counter;
int value;
char buf[64];

// ✅ 說清楚是什麼
int retry_count;
int temperature_celsius;
char device_name[64];

加一點上下文,閱讀的人不用猜。


2. 布林值(或 flag)用 is / has / enable_ 開頭

這個習慣讓 if 條件讀起來像英文句子:

// ❌
int connected;
int error;
int mode;

// ✅
bool is_connected;
bool has_error;
bool uart_tx_enable;
// 讀起來就很自然
if (is_connected && !has_error) {
    send_data();
}

比起 if (connected == 1 && error == 0),好讀太多了。


3. 函式名稱用「動詞 + 名詞」

函式是做事的,名字要反映它做了什麼動作:

// ❌ 不清楚做什麼
void sensor();
int data();
void uart();

// ✅ 動詞 + 名詞
void read_temperature_sensor();
int get_battery_voltage();
void uart_send_packet();

常用動詞參考:

  • get_ / set_ — 取得或設定某個值
  • read_ / write_ — 讀寫硬體或資料
  • init_ / deinit_ — 初始化與反初始化
  • enable_ / disable_ — 開關某個功能
  • is_ / has_ — 回傳布林值的查詢函式
  • handle_ / process_ — 處理某個事件或資料

4. 縮寫要謹慎,常見的才能縮

縮寫可以讓名字短一點,但不熟悉的縮寫反而更難讀:

// ❌ 自創縮寫,只有你看得懂
int tp;       // temperature? top? type?
void rd_cfg();
int btn_st;

// ✅ 縮寫只用大家都懂的
int temp_c;           // temperature in Celsius,還算清楚
void read_config();   // 不縮比較好
int btn_state;        // btn 算常見縮寫

我的原則是:如果你不確定同事看得懂,就不要縮


5. 常數和 Macro 全大寫加底線

這是 C 的慣例,讓你一眼就知道這是編譯期常數:

// ❌
#define bufferSize 256
#define maxRetry 3
const int timeout = 5000;

// ✅
#define BUFFER_SIZE     256
#define MAX_RETRY_COUNT 3
const int TIMEOUT_MS  = 5000;

看到全大寫,就知道這個值不會在 runtime 改變。


6. 嵌入式常見:加模組前綴

在嵌入式專案裡,C 沒有 namespace,很容易撞名。
我的習慣是用模組名稱當前綴:

// uart 模組
void uart_init(uint32_t baudrate);
void uart_send(const uint8_t *data, size_t len);
int  uart_receive(uint8_t *buf, size_t buf_size, uint32_t timeout_ms);

// sensor 模組
void    sensor_init(void);
int16_t sensor_get_temperature(void);
uint8_t sensor_get_humidity(void);

這樣就算沒有 namespace,看函式名稱也知道它屬於哪個模組。
而且 IDE 的自動補全也會把同模組的函式聚在一起,很方便。


關於匈牙利命名法

很多舊的 C 專案(尤其是 Windows API 時代)會看到這種寫法:

int iCount;       // i 代表 int
char *pszName;    // p 代表 pointer,sz 代表 null-terminated string
BOOL bIsReady;    // b 代表 bool

我個人的看法是:現代開發不太需要這樣做了

原本的用意是讓你一眼看出型別,但現在 IDE 都會直接告訴你型別,
反而是那個前綴讓名字變得難讀。

不過如果你進的公司或專案有這個慣例,就跟著走。
命名風格的一致性比「哪種風格最好」更重要。


一個實際的對比

同樣的功能,兩種寫法:

// ❌ 版本
int f(int x, int y, int *z) {
    int tmp = x * y;
    if (tmp > 100) {
        *z = 1;
        return -1;
    }
    *z = 0;
    return tmp;
}
// ✅ 版本
int calculate_power_consumption(int voltage_mv, int current_ma,
                                bool *is_over_limit) {
    int power_mw = voltage_mv * current_ma;

    if (power_mw > MAX_POWER_MW) {
        *is_over_limit = true;
        return -1;
    }

    *is_over_limit = false;
    return power_mw;
}

第二個版本,你不用看任何說明,就知道:

  • 這個函式在算功耗
  • 輸入是電壓(毫伏)和電流(毫安)
  • 有個超限的 flag
  • 回傳功耗(毫瓦),失敗回傳 -1

這就是「讓程式碼自己說話」的感覺。


Checklist

Code review 的時候可以對照一下:

  • [ ] 變數名稱能說明它「是什麼」,不需要看實作才懂
  • [ ] 布林值 / flag 有用 is_ / has_ / enable_ 開頭
  • [ ] 函式名稱有「動詞 + 名詞」的結構
  • [ ] 縮寫只用大家都懂的,不自創縮寫
  • [ ] 常數和 Macro 用全大寫加底線
  • [ ] 嵌入式模組有加前綴,避免撞名
  • [ ] 命名風格在整個檔案/專案內一致

後記

說了這麼多好習慣,但說實話,我自己也不是每次都做到。

趕 deadline 的時候還是會寫出 tmpbuf2flag3
然後三個月後再看,又是那個熟悉的「這是什麼鬼」的感覺。

我覺得命名是一個需要刻意練習的習慣,不是看完一篇文章就會的。
每次 code review 的時候,多問一句「這個名字夠清楚嗎?」,慢慢就會進步。

分類: 程式相關,標籤: , , , , , , , , , , , , , 。這篇內容的永久連結

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *