C 程式設計/常見實踐
隨著 C 語言的廣泛使用,許多常見的實踐和約定已經發展起來,以幫助避免 C 程式中的錯誤。這些同時證明了將良好的軟體工程原則應用於一種語言,也表明了 C 語言的侷限性。雖然很少有人被普遍使用,並且有些人存在爭議,但這些方法中的每一種都被廣泛使用。
雖然使用 malloc 可以輕鬆建立動態的一維陣列,並且使用內建語言功能可以輕鬆建立固定大小的多維陣列,但動態多維陣列則更加複雜。有許多不同的方法可以建立它們,每種方法都有不同的權衡。建立它們最流行的兩種方法是
- 它們可以作為一塊記憶體分配,就像靜態多維陣列一樣。這要求陣列是矩形(即低維子陣列是靜態的並且具有相同的大小)。缺點是宣告指標的語法對於初學者來說有點棘手。例如,如果想建立一個具有 3 列和行的整數陣列行,則應執行
int (*multi_array)[3] = malloc(rows * sizeof(int[3]));
- (注意這裡multi_array是指向包含 3 個整數的陣列的指標。)
- 由於陣列指標的可互換性,可以像靜態多維陣列一樣對其進行索引,即multi_array[5][2]是第 6 行第 3 列的元素。
- 動態多維陣列可以透過先分配一個指標陣列,然後分配子陣列並將它們的地址儲存在指標陣列中來分配。[1] (這種方法也稱為Iliffe 向量)。訪問元素的語法與上面描述的多維陣列相同(即使它們的儲存方式非常不同)。這種方法的優點是可以建立鋸齒狀陣列(即子陣列的大小不同)。但是,它也使用更多空間,需要更多級別的間接定址才能進行索引,並且快取效能可能更差。它還需要許多動態分配,每個動態分配都可能很昂貴。
有關更多資訊,請參閱comp.lang.c FAQ,問題 6.16。
在某些情況下,使用多維陣列最好作為結構陣列來處理。在使用者定義的資料結構可用之前,一種常見的技術是定義一個多維陣列,其中每一列包含有關該行的不同資訊。這種方法也經常被初學者使用。例如,二維字元陣列的列可能包含姓氏、名字、地址等。
在這種情況下,最好定義一個結構來包含儲存在列中的資訊,然後建立一個指向該結構的指標陣列。當給定記錄的資料點數量可能不同時,這一點尤其重要,例如專輯中的曲目。在這種情況下,最好建立一個包含專輯資訊的結構,以及專輯歌曲列表的動態陣列。然後,可以使用指向專輯結構的指標陣列來儲存集合。
- 另一種建立動態多維陣列的有用方法是將陣列展平並手動索引。例如,一個大小為 x 和 y 的二維陣列具有 x*y 個元素,因此可以透過
int dynamic_multi_array[x*y];
索引比以前稍微複雜一些,但仍然可以透過 y*i+j 獲得。然後可以使用
static_multi_array[i][j];
dynamic_multi_array[y*i+j];
來訪問陣列。一些更高維度示例
int dim1[w];
int dim2[w*x];
int dim3[w*x*y];
int dim4[w*x*y*z];
dim1[i]
dim2[w*j+i];
dim3[w*(x*i+j)+k] // index is k + w*j + w*x*i
dim4[w*(x*(y*i+j)+k)+l] // index is w*x*y*i + w*x*j + w*k + l
請注意,w*(x*(y*i+j)+k)+l 等於 w*x*y*i + w*x*j + w*k + l,但使用更少的運算(請參見霍納規則)。它使用的運算數量與透過 dim4[i][j][k][l] 訪問靜態陣列相同,因此使用起來不應該慢。
使用這種方法的優點是,陣列可以在函式之間自由傳遞,而無需在編譯時知道陣列的大小(因為 C 將其視為一維陣列,儘管仍然需要某種傳遞維度的方法),並且整個陣列在記憶體中是連續的,因此訪問連續元素應該很快。缺點是,剛開始可能很難習慣如何索引元素。
在大多數面向物件的語言中,物件不能由希望使用它們的客戶端直接建立。相反,客戶端必須要求類使用稱為建構函式的特殊例程來構建物件的例項。建構函式很重要,因為它們允許物件在其整個生命週期內強制其內部狀態的不變性。解構函式在物件生命週期的末尾被呼叫,在系統中很重要,在這些系統中,物件持有對某些資源的獨佔訪問許可權,並且希望確保它釋放這些資源供其他物件使用。
由於 C 不是面向物件的語言,因此它沒有對建構函式或解構函式的內建支援。客戶端通常顯式地分配和初始化記錄和其他物件。但是,這會導致潛在的錯誤,因為如果物件沒有正確初始化,則對物件的運算可能會失敗或行為不可預測。更好的方法是使用一個函式來建立物件的例項,該函式可能會採用初始化引數,如以下示例所示
struct string {
size_t size;
char *data;
};
struct string *create_string(const char *initial) {
assert (initial != NULL);
struct string *new_string = malloc(sizeof(*new_string));
if (new_string != NULL) {
new_string->size = strlen(initial);
new_string->data = strdup(initial);
}
return new_string;
}
類似地,如果讓客戶端正確銷燬物件,他們可能無法這樣做,從而導致資源洩漏。最好有一個始終使用的顯式解構函式,例如以下解構函式
void free_string(struct string *s) {
assert (s != NULL);
free(s->data); /* free memory held by the structure */
free(s); /* free the structure itself */
}
將解構函式與#將釋放的指標置空結合起來通常很有用。
有時隱藏物件的定義很有用,以確保客戶端不會手動分配它。為此,結構在原始檔(或使用者無法使用的私有標頭檔案)中定義,而不是在標頭檔案中定義,並在標頭檔案中進行前向宣告
struct string;
struct string *create_string(const char *initial);
void free_string(struct string *s);
如前所述,在對指標呼叫free()後,它會變成懸空指標。更糟糕的是,大多數現代平臺無法檢測到這種指標在被重新分配之前是否被使用。
一個簡單的解決方案是確保任何指標在被釋放後立即被設定為一個空指標:[2]
free(p);
p = NULL;
與懸空指標不同,當空指標被解除引用時,許多現代架構會在硬體上引發異常。此外,程式可以包含對空值的錯誤檢查,但不能包含對懸空指標值的錯誤檢查。為了確保在所有位置都執行此操作,可以使用宏
#define FREE(p) do { free(p); (p) = NULL; } while(0)
(要了解為什麼宏以這種方式編寫,請參見#宏約定。)此外,當使用這種技術時,解構函式應該將傳遞給它們的指標清零,並且它們的實參必須透過引用傳遞以允許這樣做。例如,以下來自#建構函式和解構函式的解構函式已更新
void free_string(struct string **s) {
assert(s != NULL && *s != NULL);
FREE((*s)->data); /* free memory held by the structure */
FREE(*s); /* free the structure itself */
}
不幸的是,這種習慣用法不會對可能指向已釋放記憶體的任何其他指標做任何事情。因此,一些 C 專家認為這種習慣用法很危險,因為它會產生一種錯誤的安全感。
由於 C 中的預處理器宏使用簡單的標記替換工作,因此它們容易出現許多令人困惑的錯誤,其中一些錯誤可以透過遵循一組簡單的約定來避免
- 在任何可能的地方將括號放在宏引數周圍。這樣可以確保,如果它們是表示式,則運算順序不會影響表示式的行為。例如
- 錯誤:
#define square(x) x*x - 更好:
#define square(x) (x)*(x)
- 錯誤:
- 如果它是單個表示式,則將括號放在整個表示式周圍。同樣,這避免了由於運算順序而導致的含義更改。
- 錯誤:
#define square(x) (x)*(x) - 更好:
#define square(x) ((x)*(x)) - 危險,記住它會直接替換文字。假設你的程式碼是
square (x++),在宏呼叫後,x 會增加 2
- 錯誤:
- 如果一個宏生成多個語句,或宣告變數,它可以被包裝在一個 do { ... } while(0) 迴圈中,沒有終止分號。這樣可以使宏像單個語句一樣在任何地方使用,例如 if 語句的語句體,同時仍然允許在宏呼叫後放置分號而不會建立空語句。[3][4][5][6][7][8] 必須注意,任何新變數都不能潛在地掩蓋宏引數的一部分。
- 錯誤:
#define FREE(p) free(p); p = NULL; - 更好:
#define FREE(p) do { free(p); p = NULL; } while(0)
- 錯誤:
- 儘可能避免在宏內兩次或多次使用宏引數;這會導致宏引數包含副作用(如賦值)的問題。
- 如果一個宏將來可能被函式替換,可以考慮將其命名為函式。
- 按照慣例,由
#define定義的預處理器值和宏以全大寫字母命名。[9][10][11][12][13]
進一步閱讀
[edit | edit source]- ↑ Adam N. Rosenberg. [http://www.the-adam.com/adam/rantrave/st02.pdf "A Description of One Programmer’s Programming Style Revisited"]. 2001. p. 19-20.
- ↑ comp.lang.c FAQ list: "Why isn't a pointer null after calling free?" mentions that "it is often useful to set [pointer variables] to NULL immediately after freeing them".
- ↑ "comp.lang.c FAQ: What's the best way to write a multi-statement macro?".
- ↑ "The C Preprocessor: Swallowing the Semicolon"
- ↑ "Why use apparently meaningless do-while and if-else statements in macros?"
- ↑ "do {...} while (0) in macros"
- ↑ "KernelNewbies: FAQ / DoWhile0".
- ↑ "PRE10-C. Wrap multistatement macros in a do-while loop".
- ↑ "What is the history for naming constants in all uppercase?"
- ↑ "The Preprocessor".
- ↑ "C Language Style Guide".
- ↑ "non capitalized macros are always evil".
- ↑ "Exploiting the Preprocessor for Fun and Profit".
C 語言風格指南有很多。
- "C and C++ Style Guides" by Chris Lott lists many popular C style guides.
- 汽車行業軟體可靠性協會 (MISRA) 釋出了 "MISRA-C: Guidelines for the use of the C language in critical systems"。(Wikipedia: MISRA C; [1])。