前言
剛開始寫 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 的時候還是會寫出 tmp、buf2、flag3。
然後三個月後再看,又是那個熟悉的「這是什麼鬼」的感覺。
我覺得命名是一個需要刻意練習的習慣,不是看完一篇文章就會的。
每次 code review 的時候,多問一句「這個名字夠清楚嗎?」,慢慢就會進步。