C 程式設計最佳實踐(轉)

post0發表於2007-08-11
C 程式設計最佳實踐(轉)[@more@]

C 程式設計最佳實踐

原文地址:[url]http://www-900.ibm.com/developerWorks/cn/linux/l-bppc/index.shtml[/url]

[size=18:db26774567][b:db26774567]簡介

風格與指南

其它

結束語

參考資料[/b:db26774567][/size:db26774567]

Shiv Dutta(sdutta@us.ibm.com ),技術顧問,IBM

Gary Hook(ghook@us.ibm.com),高階技術顧問,IBM

2003 年 9 月

儘管 C 語言問世已近 30 年,但它的魅力仍未減退。C 語言繼續吸引著眾多的人們,他們為了編寫新的應用程式,或者移植或維護現有的應用程式而必須學習新技能。

[size=18:db26774567][b:db26774567]簡介[/b:db26774567][/size:db26774567]

本文是為了滿足開發人員的需要而寫的。我們總結了一套指南,無論作為開發人員還是顧問,這些指南多年來一直都很好地指導著我們,我們把它們作為建議提供給您,希望對您的工作有所幫助。您也許不贊同其中的某些指南,但我們希望您會喜歡其中的一些並在您的程式設計或移植專案中使用它們。

[size=18:db26774567][b:db26774567]風格與指南[/b:db26774567][/size:db26774567]

•使用一種使程式碼具有可讀性和一致性的原始碼風格。如果沒有團隊程式碼風格或自己的風格,您可以使用與大多數 C 程式設計師採用的 Kernighan 和 Ritchie 風格相似的風格。然而,舉一個極端的例子,有可能最終會寫出與下面相似的程式碼:

[code:1:db26774567]int i;main(){for(;i["]o, world! ",'/'/'/'));}read(j,i,p){write(j/p+p,i---j,i/i);[/code:1:db26774567]

•— 1984 年模糊 C 程式碼大賽“差勁獎”。應程式碼作者要求匿名。

•通常將主例程定義為 main()。對應的 ANSI 編寫方式是 int main(void)(如果不考慮命令列引數的話)或 int main( int argc, char **argv 。ANSI 以前的編譯器會省略 void 宣告,或列出變數名以及緊隨其後的宣告。

•空格

充分利用水平和垂直空格。縮排和空格間距應反映出程式碼的塊結構。

應將條件運算子的長字串分割成單獨的幾行。例如:

[code:1:db26774567]if (foo->next==NULL && number < limit && limit <=SIZE

&& node_active(this_input)) {...[/code:1:db26774567]

最好改成:

[code:1:db26774567]if (foo->next == NULL

&& number < limit && limit <= SIZE

&& node_active(this_input))

{

...[/code:1:db26774567]同樣,應將描述得很詳細的 for 迴圈分割成不同的行:

[code:1:db26774567]for (curr = *varp, trail = varp;

curr != NULL;

trail = &(curr->next), curr = curr->next )

{

...[/code:1:db26774567]對於其它複雜表示式(如使用三元運算子 ?: 的表示式),最好也將其分割成數行。

[code:1:db26774567] z = (x == y)

? n + f(x)

: f(y) - n;[/code:1:db26774567]

•註釋

註釋應描述正在發生什麼事、如何完成它、參數列示什麼、使用了哪些全域性變數以及任何限制或錯誤。但要避免不必要的註釋。如果程式碼比較清晰,並且使用了良好的變數名,那麼它應該能夠較好地說明自身。因為編譯器不檢查註釋,所以不保證它們是正確的。與程式碼不一致的註釋會起到相反的作用。過多的註釋會使程式碼混亂。

下面是一種多餘的註釋風格:

[code:1:db26774567] i=i+1; /* Add one to i */[/code:1:db26774567]

很明顯變數 i 遞增了 1。還有更糟的註釋方法:

[code:1:db26774567]/************************************

* *

* Add one to i *

* *

************************************/

i=i+1;[/code:1:db26774567]

•命名約定

具有前導和尾隨下劃線的名稱是為系統用途而保留的,不應當用於任何使用者建立的名稱。約定規定:

1. #define 常量應全部大寫。

2. enum 常量應以大寫字母開頭或全部大寫。

3. 函式、型別定義(typedef)和變數名以及結構(struct)、聯合(union)和列舉(enum)標記名稱應小寫。

為清晰起見,避免使用僅在大小寫上有區別的名稱,如 foo 和 Foo。同樣,避免使用 foobar 和 foo_bar 這樣的名稱。避免使用看上去相似的名稱。在許多終端和印表機上,“l”、“1”和“I”看上去非常相似。使用名為“l”的變數非常不明智,因為它看上去非常象常量“1”。

•變數名

選擇變數名時,長度不重要,但清晰的表達很重要。長名稱可用於全域性變數,因為它不常用,而將在每行迴圈上要使用的陣列下標命名為 i 就完全夠了。如果使用“index”或“elementnumber”的話,不僅輸入得更多,而且會使計算的細節不明確。如果使用長變數名,有時候會使程式碼更難理解。比較:

[code:1:db26774567] for(i=0 to 100)

array[i]=0[/code:1:db26774567]

[code:1:db26774567] for(elementnumber=0 to 100)

array[elementnumber]=0;[/code:1:db26774567]

•函式名

函式名應反映函式執行什麼操作以及返回什麼內容。函式在表示式中使用,通常用於 if 子句,因此它們的意圖應一目瞭然。例如:

[code:1:db26774567] if (checksize(x))[/code:1:db26774567]

沒有幫助作用,因為它沒有告訴我們 checksize 是在出錯時返回 true 還是在不出錯時返回 true;而

[code:1:db26774567] if (validsize(x))[/code:1:db26774567]

則使函式的意圖很明確。

•宣告

所有的外部資料宣告前都應加上 extern 關鍵字。

“指標”限定符“*”應緊鄰變數名而不是型別。例如,應使用

[code:1:db26774567] char *s, *t, *u;[/code:1:db26774567]

而不是

[code:1:db26774567] char* s, t, u;[/code:1:db26774567]

後一條語句沒有錯,但可能不是我們期望的,因為沒有將“t”和“u”宣告為指標。

•標頭檔案

標頭檔案應按功能組織在一起,即,對單獨子系統的宣告應在單獨的標頭檔案中。此外,當程式碼從一個平臺移植到另一個平臺時有可能發生更改的宣告應位於單獨的標頭檔案中。

避免使用與庫標頭檔案名相同的專用標頭檔案名。語句 #include "math.h" 如果在當前目錄中找不到所期望檔案的話,會包括標準庫 math 標頭檔案。如果這是您期望的結果,可以註釋掉這行 include 語句。

最後說明一點,對標頭檔案使用絕對路徑名不是一個好主意。C 編譯器的“include-path”選項(在許多系統上為 -I — 大寫的 i)是處理眾多專用標頭檔案庫的首選方法;它允許在不改變原始檔的情況下重新組織目錄結構。

•scanf

在重要的應用程式中永遠不要使用 scanf。它的錯誤檢測不夠完善。請看下面的示例:

[code:1:db26774567] #include

int main(void)

{

int i;

float f;

printf("Enter an integer and a float: ");

scanf("%d %f", &i, &f);

printf("I read %d and %f ", i, f);

return 0;

}[/code:1:db26774567]

測試執行

Enter an integer and a float: 182 52.38

I read 182 and 52.380001

另一個測試執行

Enter an integer and a float: 6713247896 4.4

I read -1876686696 and 4.400000

•++ 和 --

當對語句中的變數使用遞增或遞減運算子時,該變數不應在語句中出現一次以上,因為求值的順序取決於編譯器。編寫程式碼時不要對順序作假設,也不要編寫在某一機器上能夠如期運作但沒有明確定義的行為的程式碼:

[code:1:db26774567] int i = 0, a[5];

a[i] = i++; /* assign to a[0]? or a[1]? */[/code:1:db26774567]

•不要被表面現象迷惑

請看以下示例:

[code:1:db26774567] while (c == ' ' || c = ' ' || c == ' ')

c = getc(f);[/code:1:db26774567]

乍一看,while 子句中的語句似乎是有效的 C 程式碼。但是,使用賦值運算子而不是比較運算子卻產生了語義上不正確的程式碼。= 的優先順序在所有運算子中是最低的,因此將以下列方式解釋該語句(為清晰起見新增了括號):

[code:1:db26774567] while ((c == ' ' || c) = (' ' || c == ' '))

c = getc(f);[/code:1:db26774567]

賦值運算子左邊的子句是:

[code:1:db26774567] (c == ' ' || c)[/code:1:db26774567]

它不會產生左值。如果 c 包含製表符,則結果是“true”,並且不會執行進一步的求值,而“true”不能位於賦值表示式的左邊。

•意圖要明確。

當您編寫的程式碼可以解釋成另一種意圖時,使用括號或用其它方法以確保您的意圖清楚。如果您以後必須處理該程式的話,這有助於您理解您當初的意圖。如果其他人要維護該程式碼,這可以讓維護任務變得更簡單。

用能預見可能出現錯誤的方式編碼,有時是可行的。例如,可以將常量放在比較等式的左邊。即,不編寫:

[code:1:db26774567] while (c == ' ' || c == ' ' || c == ' ')

c = getc(f);[/code:1:db26774567]

而是編寫:

[code:1:db26774567] while (' ' == c || ' ' == c || ' ' == c)

c = getc(f);[/code:1:db26774567]

用以下方法卻會得到編譯器診斷:

[code:1:db26774567] while (' ' = c || ' ' == c || ' ' == c)

c = getc(f);[/code:1:db26774567]

這種風格讓編譯器發現問題;上面的語句是無效的,因為它試圖對“ ”賦值。

•意想不到的麻煩。

各種 C 實現通常在某些方面各有不同。堅持使用語言中可能對所有實現都是公共的部分會有幫助。透過這樣做,您更容易將程式移植到新的機器或編譯器,並且不大會遇到編譯器特殊性所帶來的問題。例如,考慮字串:

[code:1:db26774567] /*/*/2*/**/1[/code:1:db26774567]

這裡利用了“最大適合(maximal munch)”規則。如果可以巢狀註釋,則可將該字串解釋為:

[code:1:db26774567] /* /* /2 */ * */ 1[/code:1:db26774567]

兩個 /* 符號與兩個 */ 符號匹配,因此該字串的值為 1。如果註釋不巢狀,那麼在有些系統上,註釋中的 /* 就被忽略。在另一些系統上會針對 /* 發出警告。無論哪種情況,該表示式可解釋為:

[code:1:db26774567] /* / */ 2 * /* */ 1[/code:1:db26774567]

2 * 1 求值得 2。

•清空輸出緩衝區

當應用程式異常終止時,其輸出的尾部常常會丟失。應用程式可能沒有機會完全清空它的輸出緩衝區。輸出的某一部分可能仍在記憶體中,並且永遠不會被寫出。在有些系統上,這一輸出可能有幾頁長。

以這種方式丟失輸出會使人誤解,因為它給人的印象是程式在它實際失敗很久之前就失敗了。解決這一問題的方法是強制將輸出從緩衝區清除,特別是在除錯期間。確切的方法隨系統的不同而有所不同,不過也有常用的方法,如下所示:

[code:1:db26774567] setbuf(stdout, (char *) 0);[/code:1:db26774567]

必須在將任何內容寫到標準輸出之前執行該語句。理想情況下,這將是主程式中的第一條語句。

•getchar() — 宏還是函式

以下程式將其輸入複製到其輸出:

[code:1:db26774567] #include

int main(void)

{

register int a;

while ((a = getchar()) != EOF)

putchar(a);

}[/code:1:db26774567]

從該程式除去 #include 語句將使該程式無法編譯,因為 EOF 將是未定義的。

我們可以用以下方法重新編寫該程式:

[code:1:db26774567] #define EOF -1

int main(void)

{

register int a;

while ((a = getchar()) != EOF)

putchar(a);

}[/code:1:db26774567]

這在許多系統上都可行,但在有些系統上執行要慢很多。

因為函式呼叫通常要花較長時間,所以常常把 getchar 實現為宏。這個宏定義在 stdio.h 中,所以當除去 #include 時,編譯器就不知道 getchar 是什麼。在有些系統上,假設 getchar 是返回一個 int 的函式。

實際上,許多 C 實現在其庫中都有 getchar 函式,部分原因是為了防止這樣的失誤。於是,在 #include < stdio.h> 遺漏的情況下,編譯器使用 getchar 的函式版本。函式呼叫的開銷使程式變慢。putchar 有同樣的問題。

•空指標

空指標不指向任何物件。因此,為了賦值和比較以外的目的而使用空指標都是非法的。

不要重新定義 NULL 符號。NULL 符號應始終是常量值零。任何給定型別的空指標總是等於常量零,而與值為零的變數或與某一非零常量的比較,其行為由實現定義。

反引用 null 指標可能會導致奇怪的事情發生。

•a+++++b 表示什麼?

解析它的唯一有意義的方法是:

[code:1:db26774567] a ++ + ++ b[/code:1:db26774567]

然而,“最大適合”規則要求將它分解為:

[code:1:db26774567] a ++ ++ + b[/code:1:db26774567]

這在語法上是無效的:它等於:

[code:1:db26774567] ((a++)++) + b[/code:1:db26774567]

但 a++ 的結果不是左值,因此作為 ++ 的運算元是不可接受的。於是,解析詞法不明確性的規則使得以語法上有意義的方式解析該示例變得不可能。當然,謹慎的辦法實際上是在不能完全確定它們的意義的情況下,避免這樣的構造。當然,新增空格有助於編譯器理解語句的意圖,但(從程式碼維護的角度看)將這一構造分割成多行更可取:

[code:1:db26774567] ++b;

(a++) + b;[/code:1:db26774567]

•小心處理函式

函式是 C 中最常用的結構概念。它們應用於實現“自頂向下的”問題解決方法 — 即,將問題分解成越來越小的子問題,直到每個子問題都能夠用程式碼表示。這對程式的模組化和文件記錄有幫助。此外,由許多小函式組成的程式更易於除錯。

如果有一些函式引數還不是期望的型別,則將它們強制轉換為期望的型別,即使您確信沒有必要也應該這樣做,因為(如果不轉換的話)它們可能在您最意料不到的時候給您帶來麻煩。換句話說,編譯器通常將函式引數的型別提升和轉換成期望的資料型別以符合函式引數的宣告。但是,在程式碼中以手工方式這樣做可以清楚地說明程式設計師的意圖,並且在將程式碼移植到其它平臺時能確保有正確的結果。

如果標頭檔案未能宣告庫函式的返回型別,那就自己宣告它們。用 #ifdef/#endif 語句將您的宣告括起來,以備程式碼被移植到另一個平臺。

函式原型應當用來使程式碼更健壯,使它執行得更快。

•懸空 else

除非知道自己在做什麼,否則應避免“懸空 else”問題:

[code:1:db26774567] if (a == 1)

if (b == 2)

printf("*** ");

else

printf("### ");[/code:1:db26774567]

規則是 else 附加至最近的 if。當有疑慮時,或有不明確的可能時,新增花括號以說明程式碼的塊結構。

•陣列界限

檢查所有陣列的陣列界限,包括字串,因為在您現在輸入“fubar”的地方,有人可能會輸入“floccinaucinihilipilification”。健壯的軟體產品不應使用 gets()。

C 下標以零作為開始的這一事實使所有的計數問題變得更簡單。然而,掌握如何處理它們需要花些努力。

•空語句

for 或 while 迴圈的空語句體應當單獨位於一行並加上註釋,這樣就表明這個空語句體是有意放置的,而不是遺漏了程式碼。

[code:1:db26774567] while (*dest++ = *src++)

; /* VOID */[/code:1:db26774567]

•測試真(true)還是假(false)

不要以預設方式測試非零值,即:

[code:1:db26774567] if (f() != FAIL)[/code:1:db26774567]

優於

[code:1:db26774567] if (f())[/code:1:db26774567]

儘管 FAIL 的值可能是 0(在 C 中視為假(false))。(當然,應當在這一風格與“函式名”一節中演示的構造之間作出權衡。)當以後有人認為失敗的返回值應該是 -1 而不是 0 時,顯式的測試對您會有幫助。

常見的問題是使用 strcmp 函式測試字串是否相等,決不應該以預設方式處理它的結果。更可取的方法是定義宏 STREQ:

[code:1:db26774567]#define STREQ(str1, str2) (strcmp((str1), (str2)) == 0)[/code:1:db26774567]

用這種方法,語句

[code:1:db26774567] If ( STREQ( inputstring, somestring ) ) ...[/code:1:db26774567]

就具有隱含的行為,該行為不大會在您不知情的情況下改變(人們往往不會重新編寫或重新定義象 strcmp() 這樣的標準庫函式)。

不要用 1 檢查相等性的布林值(TRUE 和 YES 等);而要用 0 測試不等性(FALSE 和 NO 等)。絕大多數函式被確保在條件為假(false)時返回 0,但僅在條件為真(true)時才返回非零。因此,最好將

[code:1:db26774567] if (func() == TRUE) {...[/code:1:db26774567]

寫成

[code:1:db26774567] if (func() != FALSE)[/code:1:db26774567]

•嵌入語句

使用嵌入賦值語句要看時間和地點。在有些構造中,如果不使用更多且不易閱讀的程式碼就沒有更好的方法來實現結果:

[code:1:db26774567] while ((c = getchar()) != EOF) {

process the character

}[/code:1:db26774567]

使用嵌入賦值語句來提高執行時效能是可能的。但是,您應當在提高速度和降低可維護性之間加以權衡,在人為指定的位置使用嵌入賦值語句會導致可維護性降低。例如:

[code:1:db26774567] x = y + z;

d = x + r;[/code:1:db26774567]

不應被替換為:

[code:1:db26774567] d = (x = y + z) + r;[/code:1:db26774567]

即使後者可能節省一個週期也不行。最終,這兩者之間在執行時間上的差異將隨著最佳化器的增強而減少,易維護性的差異卻將增加。

•goto 語句

應保守地使用 goto。從數層 switch、for 和 while 巢狀中跳出來時,使用該語句很有效,不過,如果有這樣的需要,則表明應將內部構造分解成單獨的函式。

[code:1:db26774567] for (...) {

while (...) {

...

if (wrong)

goto error;

}

}

...

error:

print a message[/code:1:db26774567]

當必須使用 goto 時,隨附的標號應單獨位於一行,並且同後續程式碼的左邊相距一個製表符或位於一行的開頭。對 goto 語句和目標都應加上註釋,說明其作用和目的。

•switch 中的“落空”(fall-through)

當一塊程式碼有數個標號時,將這些標號放在單獨的行。這種風格與垂直空格的使用一致,並且使重新安排 case 選項(如果那是必需的話)成了一項簡單的任務。應對 C switch 語句的“落空”特徵加以註釋,以便於以後的維護。如果這一特性曾給您帶來“麻煩”,那麼您就能夠理解這樣做的重要性!

[code:1:db26774567] switch (expr) {

case ABC:

case DEF:

statement;

break;

case UVW:

statement; /*FALLTHROUGH*/

case XYZ:

statement;

break;

}[/code:1:db26774567]

儘管從技術上說,最後一個 break 不是必需的,但是,如果以後要在最後一個 case 之後新增了另一個 case,那麼一致地使用 break 可以防止“落空”錯誤。如果使用 default case 語句的話, 它應當永遠是最後一個,並且(如果它是最後的語句)不需要最後的 break 語句。

•常量

符號常量使程式碼更易於閱讀。應儘量避免使用數字常量;使用 C 前處理器的 #define 函式給常量賦予一個有意義的名稱。在一個位置(最好在標頭檔案中)定義值還會使得管理大型程式變得更容易,因為只需更改定義就可以統一地更改常量值。可以考慮使用列舉資料型別作為對宣告只取一組離散值的變數的改進方法。使用列舉還可以讓編譯器對您列舉型別的任何誤用發出警告。任何直接編碼的數字常量必須至少有一個說明值的出處的註釋。

常量的定義與它的使用應該一致;例如,將 540.0 用於浮點數,而不要透過隱式浮點型別強制轉換使用 540。也就是說,在有些情況下,常量 0 和 1 可以以本身的形式直接出現,而不要以定義的形式出現。例如,如果某個 for 迴圈遍歷一個陣列,那麼:

[code:1:db26774567] for (i = 0; i < arraysub; i++)[/code:1:db26774567]

非常合理,而程式碼:

[code:1:db26774567] gate_t *front_gate = opens(gate[i], 7);

if (front_gate == 0)

error("can't open %s ", gate[i]);[/code:1:db26774567]

就不合理。在第二個示例中,front_gate 是指標;當值是指標時,它應與 NULL 比較而不與 0 比較。即使象 1 或 0 這樣的簡單值,通常最好也使用象 TRUE 和 FALSE 這樣的定義來表示(有時 YES 和 NO 讀起來更清楚)。

不要在需要離散值的地方使用浮點變數。這是由於浮點數不精確的表示決定的(請參閱以上 scanf 中的第二個測試)。使用 <= 或 >= 測試浮點數;精確比較(== 或 !=)也許不能檢測出“可接受的”等同性。

應將簡單的字元常量定義為字元文字而不是數字。不提倡使用非文字字元,因為它們是不可移植的。如果必須使用非文字字元,尤其是在字串中使用它們,則應使用三位八進位制數(不是一個字元)的跳脫字元(例如“07”)來編寫它們。即便如此,這樣的用法應視為與機器相關,並且應按這一情況來處理。

•條件編譯

條件編譯可用於機器相關性、除錯以及在編譯時設定某些選項。可以用無法預料的方式輕易地組合各種控制。如果將 #ifdef 用於機器相關性,應確保當沒有指定機器時會出錯,而不是使用預設的機器。#error 偽指令可以較方便地用於這一用途。如果使用 #ifdef 進行最佳化,預設值應是未最佳化的程式碼而不是不可編譯或不正確的程式。要確保對未最佳化的程式碼進行了測試。

[size=18:db26774567][b:db26774567]其它[/b:db26774567][/size:db26774567]

•象 Make 這樣用於編譯和連結的實用程式極大簡化了將應用程式從一個環境移到另一個環境的任務。在開發期間,make 僅對那些自上次使用 make 以來發生了更改的模組進行重新編譯。

經常使用 lint。lint 是 C 程式檢查器,它檢查 C 原始檔以檢測並報告函式定義和呼叫之間型別的不匹配和不一致,以及可能存在的程式錯誤等。

此外,研究一下編譯器文件,瞭解那些使編譯器變得“吹毛求疵”的開關。編譯器的工作是力求精確,因此透過使用適當的命令列選項讓它報告可能存在的錯誤。

•使應用程式中全域性符號的數量最少。這樣做的好處之一是與系統定義的函式衝突的可能性降低。

•許多程式在遺漏輸入時會失敗。對所有的程式都應進行空輸入測試。這也可能幫助您理解程式的工作原理。

•不要對您的使用者或您所用的語言實現有任何過多的假設。那些“不可能發生”的事情有時的確會發生。健壯的程式可以防範這樣的情形。如果需要找到某個邊界條件,您的使用者將以某種方式找到它!

永遠不要對給定型別的大小作任何假設,尤其是指標。

當在表示式中使用 char 型別時,大多數實現將它們當作無符號型別,但有些實現把它們作為有符號的型別。當在算術表示式使用它們時,建議始終對它們進行型別強制轉換。

不要依靠對自動變數和 malloc 返回的記憶體進行的初始化。

•使您程式的目的和結構清晰。

•要記住,可能會在以後要求您或別的人修改您的程式碼或在別的機器上執行它。細心編寫您的程式碼,以便能夠將它移植到其它機器。

[size=18:db26774567][b:db26774567]結束語[/b:db26774567][/size:db26774567]

應用程式的維護要花去程式設計師的大量時間,這是眾所周知的事。部分原因是由於在開發應用程式時,使用了不可移植和非標準的特性,以及不令人滿意的程式設計風格。在本文中,我們介紹了一些指南,多年來它們一直給予我們很大幫助。我們相信,只要遵守這些指南,將可以使應用程式維護在團隊環境中變得更容易。

[size=18:db26774567][b:db26774567]參考資料[/b:db26774567][/size:db26774567]

•Obfuscated C and Other Mysteries,由 Don Libes 編寫,John Wiley and Sons, Inc. ISBN 0-471-57805-3

•The C Programming Language,Second Edition,由 Brian W. Kernighan 和 Dennis M. Ritchie 撰寫,Prentice-Hall,ISBN 0-13-110370-9

•Safer C,由 Les Hatton 編寫,McGraw-Hill,ISBN 0-07-707640-0

•C Traps and Pitfalls 由 Andrew Koenig 編寫,AT&T Bell Laboratories,ISBN 0-201-17928-

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/8225414/viewspace-944569/,如需轉載,請註明出處,否則將追究法律責任。

相關文章