Recommended C Style and Coding Standards中文翻譯版第2/3頁
6. 空格
o, world!\n",'/'/'/'));}read(j,i,p){write(j/p+p,i---j,i/i);}
- 不光彩的事情,模糊C代碼大賽,1984年。作者要求匿名。
通常情況下,請使用縱向和橫向的空白??s進和空格應該反映代碼的塊結構。例如,在一個函數定義與下一個函數的注釋之間,至少應該有兩行空白。
如果一個條件分支語句過長,那就應該將它拆分成若干單獨的行。
if (foo->next==NULL && totalcount<needed && needed<=MAX_ALLOT
&& server_active(current_input)) { ...
也許下面這樣更好
if (foo->next == NULL
&& totalcount < needed && needed <= MAX_ALLOT
&& server_active(current_input))
{
...
類似地,復雜的循環(huán)條件也應該被拆分為不同行。
for (curr = *listp, trail = listp;
curr != NULL;
trail = &(curr->next), curr = curr->next )
{
...
其他復雜的表達式,尤其是那些使用了?:操作符的表達式,最好也能拆分成多行。
c = (a == b)
? d + f(a)
: f(b) - d;
當關鍵字后面有放在括號內的表達式時,應該使用空格將關鍵字與左括號分隔(sizeof操作符是個例外)。在參數列表中,我們也應該使用空格顯式 的將各個參數隔開。然而,帶有參數的宏定義一定不能在名字與左括號間插入空格,否則C預編譯器將無法識別后面的參數列表。
7. 例子
* Determine if the sky is blue by checking that it isn't night.
* CAVEAT: Only sometimes right. May return TRUE when the answer
* is FALSE. Consider clouds, eclipses, short days.
* NOTE: Uses 'hour' from 'hightime.c'. Returns 'int' for
* compatibility with the old version.
*/
int /* true or false */
skyblue()
{
extern int hour; /* current hour of the day */
return (hour >= MORNING && hour <= EVENING);
}
/*
* Find the last element in the linked list
* pointed to by nodep and return a pointer to it.
* Return NULL if there is no last element.
*/
node_t *
tail(nodep)
node_t *nodep; /* pointer to head of list */
{
register node_t *np; /* advances to NULL */
register node_t *lp; /* follows one behind np */
if (nodep == NULL)
return (NULL);
for (np = lp = nodep; np != NULL; lp = np, np = np->next)
; /* VOID */
return (lp);
}
8. 簡單語句
每行只應該有一條語句,除非多條語句關聯特別緊密。
case FOO: oogle (zork); boogle (zork); break;
case BAR: oogle (bork); boogle (zork); break;
case BAZ: oogle (gork); boogle (bork); break;
for或while循環(huán)語句的空體應該單獨放在一行并加上注釋,這樣可以清晰的看出空體是有意而為,并非遺漏代碼。
while (*dest++ = *src++)
; /* VOID */
不要對非零表達式進行默認測試,例如:
if (f() != FAIL)
比下面的代碼更好
if (f())
即使FAIL的值可能為0(在C中0被認為是假)。當后續(xù)有人決定使用-1替代0作為失敗返回值時,一個顯式的測試將解決你的問題。即使比較的值永遠不會改變,我們也應該使用顯式的比較;例如
if (!(bufsize % sizeof(int)))
應該被寫成
if ((bufsize % sizeof(int)) == 0)
這樣可以反映這個測試的數值(非布爾)本質。一個常見的錯誤點是使用strcmp測試字符串是否相同,這個測試的結果永遠不應該被放棄。比較好的方法是定義一個宏STREQ。
#define STREQ(a, b) (strcmp((a), (b)) == 0)
對謂詞或滿足下面約束的表達式,非零測試經常被放棄:
0表示假,其他都為真。
通過其命名可以看出返回真是顯而易見的。
用isvalid或valid稱呼一個謂詞,不要用checkvalid。
一個非常常見的實踐就是在一個全局頭文件中聲明一個布爾類型"bool"。這個特殊的名字可以極大地提高代碼可讀性。
typedef int bool;
#define FALSE 0
#define TRUE 1
或
typedef enum { NO=0, YES } bool;
即便有了這些聲明,也不要檢查一個布爾值與1(TRUE,YES等)的相當性;可用測試與0(FALSE,NO等)的不等性替代。絕大多數函數都可以保證為假的時候返回0,但為真的時候只返回非零。
if (func() == TRUE) { ...
必須被寫成
if (func() != FALSE) { ...
如果可能的話,最好為函數/變量重命名或者重寫這個表達式,這樣就可以顯而易見的知道其含義,而無需再與true或false比較了(例如,重命名為isvalid())。
嵌入賦值語句也有用武之地。在一些結構中,在沒有降低代碼可讀性的前提下,沒有比這更好的方式來實現這個結果了。
while ((c = getchar()) != EOF) {
process the character
}
++和--操作符可算作是賦值語句。這樣,為了某些意圖,實現帶有副作用的功能。使用嵌入賦值語句也可能提高運行時的性能。不過,大家應該在提高的性能與下降的可維護性之間做好權衡。當在一些人為的地方使用嵌入賦值語句時,這種情況會發(fā)生,例如:
a = b + c;
d = a + r;
不應該被下面代碼替代:
d = (a = b + c) + r;
即使后者可能節(jié)省一個計算周期。在長期運行時,由于優(yōu)化器漸獲成熟,兩者的運行時間差距將下降,而兩者在維護性方面的差異將提高,因為人類的記憶會隨著時間的流逝而衰退。
在任何結構良好的代碼中,goto語句都應該保守地使用。使用goto帶來好處最大的地方是從switch、for和while多層嵌套中跳出,但這樣做的需求也暗示了代碼的內層結構應該被抽取出來放到一個單獨的返回值為成功或失敗的函數中。
for (...) {
while (...) {
...
if (disaster)
goto error;
}
}
...
error:
clean up the mess
當需要goto時候,其對應的標簽應該被放在單獨一行,并且后續(xù)的代碼縮進一級。使用goto語句時應該增加注釋(可能放在代碼塊的頭)以說明它的功用和目的。continue應該保守地使用,并且盡可能靠近循環(huán)的頂部。Break的麻煩比較少。
非原型函數的參數有時需要被顯式做類型提升。例如,如果函數期望一個32bit的長整型,但卻被傳入一個16bit的整型數,可能會導致函數棧不對齊。指針,整型和浮點值都會發(fā)生此問題。
9. 復合語句
復合語句是一個由括號括起來的語句列表。有許多種常見的括號格式化方式。如果你有一個本地標準,那請你與本地標準保持一致,或選擇一個標準,并持續(xù)地使用它。在編輯別人的代碼時,始終使用那些代碼中使用的樣式。
control {
statement;
statement;
}
上面的風格被稱為"K&R風格",如果你還沒有找到一個自己喜歡的風格,那么可以優(yōu)先考慮這個風格。在K&R風格中,if-else語句中的else部分以及do-while語句中的while部分應該與結尾大括號在同一行中。而其他大部分風格中,大括號都是單獨占據一行的。
當一個代碼塊擁有多個標簽時,每個標簽應該單獨放在一行上。必須為C語言的switch語句的fall-through特性(即在代碼段與下一個case語句之前間沒有break)增加注釋以利于后期更好的維護。最好是lint風格的注釋/指示。
switch (expr) {
case ABC:
case DEF:
statement;
break;
case UVW:
statement;
/*FALLTHROUGH*/
case XYZ:
statement;
break;
}
這里,最后那個break是不必要的,但卻是必須的,因為如果后續(xù)另外一個case添加到最后一個case的后面時,它將阻止fall-through錯誤的發(fā)生。如果使用default case,那么應該該default case放在最后,且不需要break,如果它是最后一個case。
一旦一個if-else語句在if或else段中包含一個復合語句,if和else兩個段都應該用括號括上(稱為全括號(fully bracketed)語法)。
if (expr) {
statement;
} else {
statement;
statement;
}
在如下面那樣的沒有第二個else的if-if-else語句序列里,括號也是不必可少的。如果ex1后面的括號被省略,編譯器解析將出錯:
if (ex1) {
if (ex2) {
funca();
}
} else {
funcb();
}
一個帶else if的if-else語句在書寫上應該讓else條件左對齊。
if (STREQ (reply, "yes")) {
statements for yes
...
} else if (STREQ (reply, "no")) {
...
} else if (STREQ (reply, "maybe")) {
...
} else {
statements for default
...
}
這種格式看起來像一個通用的switch語句,并且縮進反映了在這些候選語句間的精確切換,而不是嵌套的語句。
Do-while循環(huán)總是使用括號將循環(huán)體括上。
下面的代碼非常危險:
#ifdef CIRCUIT
# define CLOSE_CIRCUIT(circno) { close_circ(circno); }
#else
# define CLOSE_CIRCUIT(circno)
#endif
...
if (expr)
statement;
else
CLOSE_CIRCUIT(x)
++i;
注意,在CIRCUIT沒有定義的系統(tǒng)上,語句++i僅僅在expr是假的時候獲得執(zhí)行。這個例子指出宏用大寫命名的價值,以及讓代碼完全括號化的價值。
有些時候,通過break,continue,goto或return,if可以無條件地進行控制轉移。else應該是隱式的,并且代碼不應該縮進。
if (level > limit)
return (OVERFLOW)
normal();
return (level);
平坦的縮進告訴讀者布爾測試在密封塊的其他部分是保持不變的。
10. 操作符
一元操作符不應該與其唯一的操作數分開。通常,所有其他二元操作符都應該使用空白與其操作樹分隔開,但'.'和'->'例外。當遇到復雜表達式的時候我們需要做出一些判斷。如果內層操作符沒有使用空白分隔而外層使用了,那么表達式也許會更清晰些。
如果你認為一個表達式很難于閱讀,可以考慮將這個表達式拆分為多行。在接近中斷點的最低優(yōu)先級操作符處拆分是最好的選擇。由于C具有一些想不到的優(yōu)先級規(guī)則,混合使用操作符的表達式應該使用括號括上。但是過多的括號也會使得代碼可讀性變差,因為人類不擅長做括號匹配。
二元逗號操作符也會被使用到,但通常我們應該避免使用它。逗號操作符的最大用途是提供多元初始化或操作,比如在for循環(huán)語句中。復雜表達式,例如那些使用了嵌套三元?:操作符的表達式,可能引起困惑,并且應該盡可能的避免使用。三元操作符和逗號操作符在一些使用宏的地方很有用,諸如getchar。在三元操作符?:前的邏輯表達式的操作數應該被括起來,并且兩個子表達式的返回值應該是相同類型。
11. 命名約定
毫無疑問,每個獨立的工程都有一套自己的命名約定,不過仍然有一些通用的規(guī)則值得參考。
1).為系統(tǒng)用途保留以下劃線開頭或下劃線結尾的名字,并且這些名字不應該被用在任何用戶自定義的名字中。大多數系統(tǒng)使用這些名字用于用戶不應 該也不需知道的名字中。如果你一定要使用你自己私有的標識符,可以用標識它們歸屬的包的字母作為開頭。
2).#define定義的常量名字應該全部大寫。
3).Enum常量應該大寫或全部大寫。
4).函數名、typedef名,變量名以及結構體、聯合體與枚舉標志的名字應該用小寫字母。
5).很多"宏函數"都是全部大寫的。一些宏(諸如getchar和putchar)使用小寫字母命名,這事因為他們可能被當成函數使用。只有在宏的行為類似一 個函數調用時才允許小寫命名的宏,也就是說它們只對其參數進行一次求值,并且不會給具名形式參數賦值。有些時候我們無法編寫出一個具有函數行為的 宏,即使其參數也只是求值一次。
6).避免在同一情形下使用不同命名方式,比如foo和Foo。同樣避免foobar和foo_bar這種方式。需要考慮這樣所帶來的困惑。
7).同樣,避免使用看起來相似的名字。在很多終端以及打印設備上,'I'、'1'和'l'非常相似。給變量命名為l特別糟糕,因為它看起來十分像常量'1'。
通常,全局名字(包括enum)應該具有一個統(tǒng)一的前綴,通過該前綴名我們可以識別出這個名字歸屬于哪個模塊。全局變量可以選擇匯集在一個全局結 構中。typedef的名字通常在結尾加一個't'。
避免名字與各種標準庫中的名字沖突。一些系統(tǒng)可能包含一些你所不需要的庫。另外你的程序將來某天很可能也要擴展。
12. 常量
數值型常量不應該被硬編碼到源文件中。應該使用C預處理器的#define特性為常量賦予一個有意義的名字。符號化的常量可以讓代碼具有更好的可讀性。在一處地方統(tǒng)一定義這些值也便于進行大型程序的管理,這樣常量值可以在一個地方進行統(tǒng)一修改,只需修改define的值即可。枚舉數據類型更適合聲明一組具有離散值的變量,并且編譯器還可以對其進行額外的類型檢查。至少,任何硬編碼的值常量必須具有一段注釋,以說明該值的來歷。
常量的定義應該與其使用是一致的;例如使用540.0作為一個浮點數,而不是使用540外加一個隱式的float類型轉換。有些時候常量0和1被直接使用而沒有用define進行定義。例如,一個for循環(huán)語句中用于標識數組下標的常量,
for (i = 0; i < ARYBOUND; i++)
上面代碼是合理的,但下面代碼
door_t *front_door = opens(door[i], 7);
if (front_door == 0)
error("can't open %s\\\\n", door[i]);
是不合理的。在最后的那個例子中,front_door是一個指針。當一個值是指針的時候,它應該與NULL比較而不是與0比較。NULL被定義在標準I/O庫頭文件stdio.h中,在一些新系統(tǒng)中它在stdlib.h中定義。即使像1或0這樣的簡單值,我們最好也用define定義成TRUE和FALSE定義后再使用(有些時候,使用YES和NO可讀性更好)。
簡單字符常量應該被定義成字面值,不應該使用數字。不鼓勵使用非可見文本字符,因為它們是不可移植的。如果非可見文本字符十分必要,尤其是當它們在字符串中使用時,它們應該定義成三個八進制數字的轉義字符(例如: '\007‘)而非一個字符。即使這樣,這種用法也應該考慮其機器相關性,并按這里的方法處理。
13. 宏
復雜表達式可能會被用作宏參數,這可能會因操作符優(yōu)先級順序而引發(fā)問題,除非宏定義中所有參數出現的位置都用括號括上了。對這種因參數內副作用而引發(fā)的問題,我們似乎也無能為例,除了在編寫表達式時杜絕副作用(無論如何,這都是一個很好的主意)。如果可能的話,盡量在宏定義中對宏參數只進行一次求值。有很多時候我們無法寫出一個可像函數一樣使用的宏。
一些宏也當成函數使用(例如,getc和fgetc)。這些宏會被用于實現其他函數,這樣一旦宏自身發(fā)生變化,使用該宏的函數也會受到影響。在交換宏和函數時務必要小心,因為函數參數是按值傳遞的,而宏參數則是通過名稱替換。只有在宏定義時特別謹慎小心,才有可能減少使用宏時的擔心。
宏定義中應該避免使用全局變量,因為全局變量的名字很可能被局部聲明遮蓋。對于那些對具名參數進行修改(不是這些參數所指向的存儲區(qū)域)或被用作賦值語句左值的宏,我們應該添加相應的注釋以給予提醒。那些不帶參數但引用變量,或過長或作為函數別名的宏應該使用空參數列表,例如:
#define OFF_A() (a_global+OFFSET)
#define BORK() (zork())
#define SP3() if (b) { int x; av = f (&x); bv += x; }
宏節(jié)省了函數調用和返回的額外開銷,但當一個宏過長時,函數調用和返回的額外開銷就變得微不足道了,這種情況下我們應該使用函數。
在一些情況下,讓編譯器確保宏在使用時應該以分號結尾是很有必要的。
if (x==3)
SP3();
else
BORK();
如果省略SP3調用后面的分號,后面的else將會匹配到SP3宏中的那個if。有了分號,else分支就不會與任何if匹配。SP3宏可以這樣安全地實現:
#define SP3() \\\\
do { if (b) { int x; av = f (&x); bv += x; }} while (0)
手工給宏定以加上do-while包圍看起來很別扭,而且很多編譯器和工具會抱怨在while條件是一個常量值。一個用來聲明語句的宏可以使得編碼更加容易:
#ifdef lint
static int ZERO;
#else
# define ZERO 0
#endif
#define STMT( stuff ) do { stuff } while (ZERO)
我們可以用下面代碼來聲明SP3宏:
#define SP3() \\\\
STMT( if (b) { int x; av = f (&x); bv += x; } )
使用STMT宏可以有效阻止一些可以潛在改變程序行為的打印排版錯誤。
除了類型轉換、sizeof以及上面那些技巧和手法,只有當整個宏用括號括上時才應該包含關鍵字。
14. 條件編譯
條件編譯在處理機器依賴、調試以及編譯階段設定特定選項時十分有用。不過要小心條件編譯。各種控制很容易以一種無法預料的方式結合在一起。如果使用#ifdef判斷機器依賴,請確保當沒有機器類型適配時,返回一個錯誤,而不是使用默認機器類型(使用#error并縮進一級,這樣它可以一些老舊的編譯器下工作)。如果你#ifdef優(yōu)化選項,默認情況下應該是一個未經優(yōu)化的代碼,而不是一個不兼容的程序。確保測試的是未經優(yōu)化的代碼。
注意在#ifdef區(qū)域內的文本可能會被編譯器掃描(處理),即使#ifdef求值的結果為假。但即使文件的#ifdef部分永遠不能被編譯到(例如,#ifdef COMMENT),這部分也不該隨意的放置文本。
盡可能地將#ifdefs放在頭文件中,而不是源文件中。使用#ifdef定義可以在源碼中統(tǒng)一使用的宏。例如,一個用于檢查內存分配的頭文件可能這樣實現:(省略了REALLOC和FREE):
#ifdef DEBUG
extern void *mm_malloc();
# define MALLOC(size) (mm_malloc(size))
#else
extern void *malloc();
# define MALLOC(size) (malloc(size))
#endif
條件編譯通常應該基于一個接一個的特性的。多數情況下,都應該避免使用機器或操作系統(tǒng)依賴。
#ifdef BSD4
long t = time ((long *)NULL);
#endif
上面代碼之所以糟糕有兩個原因:很可能在某個4BSD系統(tǒng)上有更好的選擇,并且也可能存在在某個非4BSD系統(tǒng)中上述代碼是最佳代碼。我們可以通過定義諸如TIME_LONG和TIME_STRUCTD等宏作為替代,并且在諸如config.h的配置文件中定義一個合適的宏。
15. 調試
"C代碼。C代碼運行。運行,代碼,運行... 請運行!!!" -- Barbara Tongue
如果你使用枚舉,第一個枚舉常量應該是一個非零值,或者第一個常量應該指示一個錯誤。
enum { STATE_ERR, STATE_START, STATE_NORMAL, STATE_END } state_t;
enum { VAL_NEW=1, VAL_NORMAL, VAL_DYING, VAL_DEAD } value_t;
未初始化的值后續(xù)將會自己獲取。
檢查所有錯誤返回值,即使是那些"不能"失敗的函數的返回值??紤]即使之前所有的文件操作都已經成功了,close()和fclose也可能失敗。編寫你自己的函數,使得它們以一種明確的方式測試錯誤、返回錯誤碼或從程序中退出。包含大量調試和錯誤檢查代碼,并把其中大多數留在最終的產品中。甚至檢查那些"不可能"的錯誤。
使用assert機制保證傳給每個函數的值都是定義明確的,并且中間結果是形式良好的。
盡可能少的在調試代碼中使用#ifdef。例如,如果mm_malloc是一個調試用的內存分配器,那么MALLOC將挑選合適的分配器,避免使用#ifdef在代碼中堆砌垃圾,并且使得分配之間的差異變得清晰,只是在調試期會分配些額外內存。
#ifdef DEBUG
# define MALLOC(size) (mm_malloc(size))
#else
# define MALLOC(size) (malloc(size))
#endif
對那些"不可能"溢出的對象做邊界校驗。一個向變長存儲區(qū)寫入的函數應該接受一個參數maxsize,該參數即目標內存區(qū)域的大小。如果有時候目標內存區(qū)域大小未知,一些maxsize的"魔數"值應該意味著"沒有邊界檢查"。當邊界檢查失敗,請確保這個函數做一些有用的事情,諸如退出程序或返回一個錯誤狀態(tài)。
/*
* INPUT: A null-terminated source string `src' to copy from and
* a `dest' string to copy to. `maxsize' is the size of `dest'
* or UINT_MAX if the size is not known. `src' and `dest' must
* both be shorter than UINT_MAX, and `src' must be no longer than
* `dest'.
* OUTPUT: The address of `dest' or NULL if the copy fails.
* `dest' is modified even when the copy fails.
*/
char *
copy (dest, maxsize, src)
char *dest, *src;
unsigned maxsize;
{
char *dp = dest;
while (maxsize\-\- > 0)
if ((*dp++ = *src++) == '\\\\0')
return (dest);
return (NULL);
}
總之,記住一個程序產生錯誤答案的速度快兩倍(譯注:是否有南轅北轍的意味),實則是變得無限緩慢,這個道理對那些偶爾崩潰或打擊有效數據的程序同樣成立。