C:慣用效率參考指南
目錄
1. 記憶體管理 {#memory}
// ❌ 未檢查回傳值就使用 malloc
char *buf = malloc(size);
strcpy(buf, src);
// ✅
char *buf = malloc(size);
if (!buf) return -ENOMEM;
memcpy(buf, src, size);
// ❌ 對 malloc 結果進行型別轉換 (C 語言中非必要,會隱藏遺漏 #include 的問題)
int *p = (int *)malloc(n * sizeof(int));
// ✅
int *p = malloc(n * sizeof *p);
// ❌ free 後未設為 NULL (在長生命週期的 scope 中有懸空指標風險)
free(ptr);
// ... 後續程式碼可能用到 ptr
// ✅
free(ptr);
ptr = NULL;
// ❌ 在提早回傳的路徑上忘記釋放記憶體
char *a = malloc(100);
char *b = malloc(200);
if (!b) return -1; // 洩漏了 a
// ✅ — 單一清理標籤
char *a = NULL, *b = NULL;
a = malloc(100);
if (!a) goto cleanup;
b = malloc(200);
if (!b) goto cleanup;
// ... 使用 a, b ...
cleanup:
free(b);
free(a);
在 malloc 中使用 sizeof *ptr 而非 sizeof(Type) — 當型別改變時仍能保持正確。
2. 指標與陣列 {#pointers}
// ❌ 手動追蹤陣列大小
void process(int *arr, int len) { ... }
process(data, 10);
// ✅ — 將大小與指標一起傳遞,或使用結構體封裝
typedef struct { int *data; size_t len; } IntSlice;
// ❌ 使用陣列索引更清晰時卻用指標算術
*(arr + i) = value;
// ✅
arr[i] = value;
// ❌ 在正式程式碼中使用 VLA (有堆疊溢位風險,在 C11+ 中為可選特性)
int arr[n];
// ✅
int *arr = malloc(n * sizeof *arr);
if (!arr) return -ENOMEM;
// ... 使用 arr ...
free(arr);
3. 錯誤處理 {#errors}
// ❌ 使用魔術數字表示錯誤回傳值
if (do_thing() == -1) { ... }
// ✅ — 定義或使用具名錯誤碼
#include <errno.h>
if (do_thing() < 0) {
perror("do_thing");
return errno;
}
// ❌ 深度巢狀的錯誤檢查
int r1 = step1();
if (r1 == 0) {
int r2 = step2();
if (r2 == 0) {
int r3 = step3();
// ...
}
}
// ✅ — 提早回傳 / goto cleanup
if (step1() < 0) goto fail;
if (step2() < 0) goto fail;
if (step3() < 0) goto fail;
return 0;
fail:
cleanup();
return -1;
goto cleanup 是 C 語言中用於資源清理的慣用寫法 — 不要因為原則性原因而避免使用它。
4. 字串 {#strings}
// ❌ 未做邊界檢查的 strcpy
strcpy(dest, src);
// ✅
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';
// 或更好:snprintf(dest, sizeof(dest), "%s", src);
// ❌ strcmp 誤用
if (str == "hello") { ... } // 比較的是指標,而非內容
// ✅
if (strcmp(str, "hello") == 0) { ... }
// ❌ 使用重複的 strcat 組合字串 (O(n²))
char result[1024] = "";
for (int i = 0; i < n; i++) {
strcat(result, items[i]);
}
// ✅ — 追蹤寫入位置
char result[1024];
int pos = 0;
for (int i = 0; i < n && pos < (int)sizeof(result); i++) {
pos += snprintf(result + pos, sizeof(result) - pos, "%s", items[i]);
}
永遠優先使用 snprintf 而非 sprintf。
5. 結構體與列舉 {#structs}
// ❌ 赤裸裸的結構體,到處都需要 `struct` 關鍵字
struct point { int x, y; };
struct point p = {1, 2};
// ✅
typedef struct { int x, y; } Point;
Point p = {1, 2};
// ❌ 未初始化的結構體
Point p;
use(p.x); // 未定義行為
// ✅
Point p = {0};
// ❌ 魔術整數常數
if (state == 3) { ... }
// ✅
typedef enum { STATE_IDLE, STATE_RUNNING, STATE_DONE } State;
if (state == STATE_DONE) { ... }
6. 前置處理器與標頭檔 {#preprocessor}
// ❌ 使用可用行內函數替代的巨集 (缺乏型別安全,雙重求值)
#define MAX(a, b) ((a) > (b) ? (a) : (b))
MAX(x++, y) // 若 x > y,x 會遞增兩次
// ✅
static inline int max_int(int a, int b) { return a > b ? a : b; }
// ❌ 沒有引入防護
// my_header.h
struct Foo { int x; };
// ✅
#ifndef MY_HEADER_H
#define MY_HEADER_H
struct Foo { int x; };
#endif
// 或: #pragma once (廣泛支援,非標準)
將巨集保留給條件式編譯和常數。邏輯部分使用 static inline。
7. C 語言特定反模式 {#antipatterns}
| 反模式 | 建議寫法 |
|---|---|
sprintf | snprintf 並帶入緩衝區大小 |
gets | fgets (gets 已在 C11 中移除) |
對 malloc 結果進行型別轉換 | 讓隱含的 void* 轉換生效 |
在 malloc 中使用 sizeof(Type) | sizeof *ptr |
| 對大型/執行時期陣列使用 VLA | 堆積 (heap) 配置 |
無 context 參數的 void* 回呼 | 連同函數指標一起傳遞 void *ctx |
| 全域可變狀態 | 透過結構體指標傳遞狀態 |
使用 assert 處理執行時期錯誤 | 使用適當的錯誤回傳碼 |
唯讀指標參數遺漏 const | const char *str |
| 在比較中混合有號/無號型別 | 使用一致型別,明確轉型 |
限制
- 這些是語言特定的指南,不涵蓋整體架構決策。
- 過度壓縮可能降低可讀性;請自行判斷。