0. 摘要
本文翻譯自《Recommended C Style and Coding Standards》。
作者資訊:
L.W. Cannon (Bell Labs)
R.A. Elliott (Bell Labs)
L.W. Kirchhoff (Bell Labs)
J.H. Miller (Bell Labs)
J.M. Milner (Bell Labs)
R.W. Mitze (Bell Labs)
E.P. Schan (Bell Labs)
N.O. Whittington (Bell Labs)
Henry Spencer (Zoology Computer Systems, University of Toronto)
David Keppel (EECS, UC Berkeley, CS&E, University of Washington)
Mark Brader (SoftQuad? Incorporated, Toronto)
本文是《Indian Hill C Style and Coding Standards》的更新版本,上面提到的最後三位作者對其進行了修改。本文主要介紹了一種C程式的推薦編碼通訊協定,內容著重於講述編碼風格,而不是功能 組織(Functional Organization)。
1. 簡介
本文檔修改於AT&T Indian Hill實驗室內部成立的一個委員會的一份文檔,旨在於建立一套通用的編碼通訊協定並推薦給Indian Hill社區。
本文主要講述編碼風格。良好的風格能夠鼓勵大家形成一致的代碼布局,提高代碼可移植性並且減少錯誤數量。
本文不關注功能組織,或是一些諸如如何使用goto的一般話題。我們嘗試將之前的有關C代碼風格的文檔整合到一套統一的標準中,這套標準將適合於 任何使用C語言的工程,當然還是會有部分內容是針對一些特定系統的。另外不可避免地是這些標準仍然無法覆蓋到所有情況。經驗以及廣泛的評價十分重 要,遇到特殊情況時,大家應該諮詢有經驗的C程式員,或者查看那些經驗豐富的C程式員們的代碼(最好遵循這些規則)。
本文中的標準本身並不是必需的,但個別機構或團體可能部分或全部採用該標準作為程式驗收的一部分。因此,在你的機構中其他人很可能以一種相似的風 格編碼。最終,這些標準的目的是提高可移植性,減少維護工作,尤其是提高代碼的清晰度。
這裡很多風格的選擇都有些許武斷。混合的編碼風格比糟糕的編碼風格更難於維護,所以當變更現有代碼時,最好是保持與現有代碼風格一致,而不是盲目 地遵循本文檔中的規則。
"清晰的是專業的;不清晰的則是外行的" — Sir Ernest Gowers
2. 檔案組織
一個檔案包含的各個部分應該用若干個空行分隔。雖然對源檔案沒有最大長度限制,但超過1000行的檔案處理起來非常不方便。編輯器很可能沒有足夠 的臨時空間來編輯這個檔案,編譯過程也會因此變得十分緩慢。與復原到前面所花費的時間相比,那些僅僅呈現了極少量資訊的多行星號是不值得的,我們 不鼓勵使用。超過79列的行無法被所有的終端都很好地處理,應該儘可能的避免使用。過長的行會導致過深的縮排,這常常是一種程式碼群組織不善的癥狀。
2.1 檔案命名慣例
檔案名稱由一個基礎名、一個可選的句號以及尾碼組成。名字的第一個字元應該是一個字母,並且所有字元(除了句號)都應該是小寫字母和數字。基礎名 應該由八個或更少的字元組成,尾碼應該由三個或更少的字元組成(四個,如果你包含句號的話)。這些規則對程式檔案以及程式使用和產生的預設檔案都 適用(例如,"rogue.sav")。
一些編譯器和工具要求檔案名稱符合特定的尾碼命名規範。下面是尾碼命名要求:
C源檔案的名字必須以.c結尾
彙編源檔案的名字必須以.s結尾
我們普遍遵循以下命名規範:
可重定位目標檔案名以.o結尾
標頭檔名以.h結尾
在多語言環境中一個可供選擇的更好的約定是用語言類型和.h共同作為尾碼(例如,"foo.c.h" 或 "foo.ch")。
Yacc源檔案名稱以.y結尾
Lex源檔案名稱以.l結尾
C++使用編譯器相關的尾碼約定,包括.c,..c,.cc,.c.c以及.C。由於大多C代碼也是C++代碼,因此這裡並沒有一個明確的方案。
此外,我們一般約定使用"Makefile"(而不是"makefile")作為make(對於那些支援make的系統)工具的控制檔案,並且使 用"README"作為簡要描述目錄內容或分類樹的檔案。
2.2 程式檔案
下面是一個程式檔案各個組成部分的推薦排列順序:
檔案的第一部分是一個序,用於說明該檔案中的內容是什麼。對檔案中的對象(無論它們是函數,外部資料聲明或定義,或是其他一些東西)用途的描述比 一個對象名字列表更加有用。這個序可選擇地包含作者資訊、修訂控制資訊以及參考資料等。
接下來是所有被包含的標頭檔。如果某個標頭檔被包含的理由不是那麼顯而易見,我們需要通過增加註釋說明原因。大多數情況下,類似stdio.h這 樣的系統標頭檔應該被放在使用者自訂標頭檔的前面。
接下來是那些用於該檔案的defines和typedefs。一個常規的順序是先寫常量宏、再寫函數宏,最後是typedefs和枚舉 (enums)定義。
接下來是全域(外部)資料聲明,通常的順序如下:外部變數,非靜態(non-static)全域變數,靜態全域變數。如果一組定義被用於部分特定 全域資料(如一個標誌字),那麼這些定義應該被放在對應資料聲明後或嵌入到結構體聲明中,並將這些定義縮排到其應用的聲明的第一個關鍵字的下一個 層次(譯註:實在沒有搞懂後面這句的含義)。
最後是函數,函數應該以一種有意義的順序排列。相似的函數應該放在一起。與深度優先(函數定義儘可能在他們的調用者前後)相比,我們應該首選廣度 優先方法(抽象層次相似的函數放在一起)。這裡需要相當多的判斷。如果定義大量本質上無關的工具函數,可考慮按字母表順序排列。
2.3 標頭檔
標頭檔是那些在編譯之前由C前置處理器包含在其他檔案中的檔案。諸如stdio.h的一些標頭檔被定義在系統層級,所有使用標準I/O庫的程式必須 包含它們。標頭檔還用來包含資料聲明和定義,這些資料不止一個程式需要。標頭檔應該按照功能組織,例如,獨立子系統的聲明應該放到獨立的標頭檔 中。如果一組聲明在代碼從一種機器移植到另外一種機器時變動的可能性很大,那麼這些聲明也應該被放在獨立的標頭檔中。
避免私人標頭檔的名字與標準庫標頭檔的名字一樣。下面語句:
複製代碼 代碼如下:
#include "math.h"
當預期的標頭檔在目前的目錄下沒有找到時,它將會包含標準庫中的math標頭檔。如果這的確是你所期望發生的,那麼請加上注釋。包含標頭檔時不要使 用絕對路徑。當從標準位置擷取標頭檔時,請使用<name>包含標頭檔;或相對於當前路徑定義它們。C編譯器的"include- path"選項(在許多系統中為-l)是處理擴充私人庫標頭檔的最好方法,它允許在不改變源碼檔案的情況下重新組織目錄結構。
聲明了函數或外部變數的標頭檔應該被那些定義了這些函數和變數的檔案所包含。這樣一來,編譯器就可以做類型檢查了,並且外部聲明將總是與定義保持 一致。
在標頭檔中定義變數往往是個糟糕的想法,它經常是一個在檔案間對代碼進行低劣劃分的癥狀。此外,在一次編譯中,像typedef和經過初始化的數 據定義無法被編譯器看到兩次。在一些系統中,重複的沒有使用extern關鍵字修飾的未初始化定義也會導致問題。當標頭檔嵌套時,會出現重複的聲 明,這將導致編譯失敗。
標頭檔不應該嵌套。一個標頭檔的序應該描述其使用的其他被包含的標頭檔的實用特性。在極特殊情況下,當大量標頭檔需要被包含在多個不同的源檔案中 時,可以被接受的做法是將公用的標頭檔包含在一個單獨的標頭檔中。
一個通用的做法是將下面這段代碼加入到每個標頭檔中以防止標頭檔被意外多次包含。
複製代碼 代碼如下:
#ifndef EXAMPLE_H
#define EXAMPLE_H
… /* body of example.h file */
#endif /* EXAMPLE_H */
我們不應該對這種避免多次包含的機制產生依賴,特別是不應該因此而嵌套包含標頭檔。
2.4 其他檔案
還有一個慣例就是編寫一個名為"README"的檔案,用於描述程式的整體情況以及問題。例如,我們經常在README包含程式所使用的條件編譯 選項列表以及相關說明,還可以包含機器無關的檔案清單等。
3. 注釋
"當代碼與注釋不一致時,兩者很可能都是錯的" -- Norm Schryer
注釋應該描述發生了什麼,如何做的,參數的含義,使用和修改了哪些全域變數以及約束或Bugs。避免給那些本身很清晰的代碼加註釋,因為這些注釋資訊將很快的過時。注釋與代碼不一致將會帶來負面影響。短小的注釋應該是關於做什麼的,比如"計算有意義的值",而不是關於"怎麼做"的,例如"值的總和除以n"。C不是彙編;在頭3-10行的地區添加註釋,說明代碼總體是做什麼的,經常要比為每行添加註釋說明微邏輯更加有用。
注釋應該為那些令人不悅的代碼作出"辯護"。辯護應該是這樣的:如果使用正常的代碼,一些糟糕的事情將會發生。但僅僅讓代碼啟動並執行更快還不足以讓這些hack代碼顯得合理化;而是應該將那些在不使用hack代碼時令人無法接受的效能資料展示出來。注釋應該對著寫不可接受的行為作出解釋,並告訴大家為什麼使用Hack代碼可以很好的解決這個問題。
那些用於描述資料結構,演算法等的注釋應該以塊注釋的形式存在。塊注釋起始以/*佔據1-2列,*放在每行注釋前面的第二列,塊注釋最後以佔據2-3列的*/結尾。另外一個候選方案是每行注釋文字前面用*佔據1-2列,塊注釋以佔據1-2列的*/收尾。
複製代碼 代碼如下:
/*
* Here is a block comment.
* The comment text should be tabbed or spaced over uniformly.
* The opening slash-star and closing star-slash are alone on a line.
*/
/*
** Alternate format for block comments
*/
注意
複製代碼 代碼如下:
grep '^.\e*'
將匹配檔案中所有的注釋。特別長的塊注釋,諸如持久討論或著作權聲明,經常以佔據1-2列的/*開始,每行注釋文字前沒有*,並最終以佔據1-2列的*/結束。函數內部很適合使用塊注釋,塊注釋應該與其描述的代碼擁有相同的縮排。獨立的單行注釋也應該與其說明的代碼縮排一致。
複製代碼 代碼如下:
if (argc > 1) {
/* Get input file from command line. */
if (freopen(argv[1], "r", stdin) == NULL) {
perror (argv[1]);
}
}
特別短的注釋可以與其描述的代碼放在同一行上,並且要通過tab與代碼語句分隔開來。如果針對一塊代碼有不止一個短注釋,這些注釋應該具有相同的縮排。
複製代碼 代碼如下:
if (a == EXCEPTION) {
b = TRUE; /* special case */
} else {
b = isprime(a); /* works only for odd a */
}
4. 聲明
全域聲明應該從第一列開始。在所有外部資料聲明的前面都應該放置extern關鍵字。如果一個外部變數是一個在定義時大小確定的數組,那麼這個數 組界限必須在extern聲明時顯示指出,除非數組的大小與數組本身編碼在一起了(例如,一個總是以0結尾的唯讀字元數組)。重複聲明數組大小對 於一些使用他人編寫的代碼的人特別有益。
指標修飾符*應該與變數名在一起,而不是與類型在一起。
複製代碼 代碼如下:
char *s, *t, *u;
替換
複製代碼 代碼如下:
char* s, t, u;
後者是錯誤的,因為實際上t和u並未如預期那樣被聲明為指標。
不相關的聲明,即使是相同類型的,也應該獨立佔據一行。我們應該對聲明對象的角色進行注釋,不過當常量名本身足以說明角色時,使用#define 定義的常量列表則不需要注釋。通常多行變數名、值與注釋使用相同縮排,使得他們在一列直線上。盡量使用Tab字元而不是空格。結構體和聯合體的聲 明時,每個元素應該單獨佔據一行,並附帶一條注釋。{應該與結構體的tag名放在同一行,}應該放在聲明結尾的第一列。
複製代碼 代碼如下:
struct boat {
int wllength; /* water line length in meters */
int type; /* see below */
long sailarea; /* sail area in square mm */
};
/* defines for boat.type */
#define KETCH (1)
#define YAWL (2)
#define SLOOP (3)
#define SQRIG (4)
#define MOTOR (5)
這些defines有時放在結構體內type聲明的後面,並使用足夠的tab縮排到結構體成員成員的下一級。如果這些實際值不那麼重要的話,使用 enum會更好。
複製代碼 代碼如下:
enum bt { KETCH=1, YAWL, SLOOP, SQRIG, MOTOR };
struct boat {
int wllength; /* water line length in meters */
enum bt type; /* what kind of boat */
long sailarea; /* sail area in square mm */
};
任何初值重要的變數都應該被顯式地初始化,或者至少應該添加註釋,說明依賴C的預設初始值0。空初始化"{}"應該永遠不被使用。結構體初始化應 該用大括弧完全括起來。用於初始化長整型(long)的常量應該使用顯式長度。使用大寫字母,例如2l看起來更像21,數字二十一。
複製代碼 代碼如下:
int x = 1;
char *msg = "message";
struct boat winner[] = {
{ 40, YAWL, 6000000L },
{ 28, MOTOR, 0L },
{ 0 },
};
如果一個檔案不是獨立程式,而是某個工程整體的一部分,那麼我們應該最大化的利用static關鍵字,使得函數和變數對於單個檔案來說是局部範疇 的。只有在有清晰需求且無法通過其他方式實現的特殊情況時,我們才允許變數被其他檔案訪問。這種情況下應該使用注釋明確告知使用了其他檔案中的變 量;注釋應該說明其他檔案的名字。如果你的調試器遮蔽了你需要在調試階段查看的靜態對象,那麼可以將這些變數聲明為STATIC,並根據需要決定 是否#define STATIC。
最重要的類型應該被typedef,即使他們只是整型,因為獨立的名字使得程式更加易讀(如果只有很少的幾個integer的typedef)。 結構體在聲明時應該被typedef。保持結構體標誌的名字與typedef後的名字相同。
複製代碼 代碼如下:
typedef struct splodge_t {
int sp_count;
char *sp_name, *sp_alias;
} splodge_t;
總是聲明函數的傳回型別。如果函數原型可用,那就使用它。一個常見的錯誤就是忽略那些返回double的外部數學函式宣告。那樣的話,編譯器就會 假定這些函數的傳回值為一個整型數,並且將bit位逐一盡職盡責的注意轉換為一個浮點數(無意義)。
"C語言的觀點之一是程式員永遠是對的" — Michael DeCorte
5. 函式宣告
每個函數前面應該放置一段塊注釋,概要描述該函數做什麼以及(如果不是很清晰)如何使用該函數。重要的設計決策討論以及副作用說明也適合放在注釋 中。避免提供那些代碼本身可以清晰提供的資訊。
函數的傳回型別應該單獨佔據一行,(可選的)縮排一個層級。不用使用預設傳回型別int;如果函數沒有傳回值,那麼將傳回型別聲明為void。如 果傳回值需要大段詳細的說明,可以在函數之前的注釋中描述;否則可以在同一行中對傳回型別進行注釋。函數名(以及形式參數列表)應該被單獨放在一 行,從第一列開始。目的(傳回值)參數一般放在第一個參數位置(從左面開始)。所有形式參數聲明、局部聲明以及函數體中的代碼都應該縮排一級。函 數體的開始括弧應該單獨一行,放在開始處的第一列。
每個參數都應該被聲明(不要使用預設類型int)。通常函數中每個變數的角色都應該被描述清楚,我們可以在函數注釋中描述,或如果每個聲明單獨一 行,我們可以將注釋放在同一行上。像迴圈計數器"i",字串指標"s"以及用於標識字元的整數類型"c"這些簡單變數都無需注釋。如果一組函數 都擁有一個相似的參數或局部變數,那麼在所有函數中使用同一個名字來標識這個變數是很有益處的(相反,避免在相關函數中使用一個名字標識用途不同 的變數)。不同函數中的相似參數還應該放在各個參數列表中的相同位置。
參數和局部變數的注釋應該統一縮排以排成一列。局部變數聲明應用一個空行與函數語句分隔開來。
當你使用或聲明變長參數的函數時要小心。目前在C中尚沒有真正可移植的方式處理變長參數。最好設計一個使用固定個數參數的介面。如果一定要使用變 長參數,請使用標準庫中的宏來聲明具有變長參數的函數。
如果函數使用了在檔案中沒有進行全域聲明的外部變數(或函數),我們應該在函數體內部使用extern關鍵字單獨對這些變數進行聲明。
避免局部聲明覆蓋進階別的聲明。尤其是,局部變數不應該在嵌套代碼塊中被重聲明。雖然這在C中是合法的,但是當使用-h選項時,潛在的衝突可能性 足以讓lint工具發出抱怨之聲。
當前1/3頁
123下一頁閱讀全文