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程序員們的代碼(最好遵循這些規則)。
本文中的標準本身并不是必需的,但個別機構或團體可能部分或全部采用該標準作為程序驗收的一部分。因此,在你的機構中其他人很可能以一種相似的風 格編碼。最終,這些標準的目的是提高可移植性,減少維護工作,尤其是提高代碼的清晰度。
這里很多風格的選擇都有些許武斷?;旌系木幋a風格比糟糕的編碼風格更難于維護,所以當變更現有代碼時,最好是保持與現有代碼風格一致,而不是盲目 地遵循本文檔中的規則。
"清晰的是專業的;不清晰的則是外行的" ― Sir Ernest Gowers
2. 文件組織
一個文件包含的各個部分應該用若干個空行分隔。雖然對源文件沒有最大長度限制,但超過1000行的文件處理起來非常不方便。編輯器很可能沒有足夠 的臨時空間來編輯這個文件,編譯過程也會因此變得十分緩慢。與回滾到前面所花費的時間相比,那些僅僅呈現了極少量信息的多行星號是不值得的,我們 不鼓勵使用。超過79列的行無法被所有的終端都很好地處理,應該盡可能的避免使用。過長的行會導致過深的縮進,這常常是一種代碼組織不善的癥狀。
2.1 文件命名慣例
文件名由一個基礎名、一個可選的句號以及后綴組成。名字的第一個字符應該是一個字母,并且所有字符(除了句號)都應該是小寫的字母和數字?;A名 應該由八個或更少的字符組成,后綴應該由三個或更少的字符組成(四個,如果你包含句號的話)。這些規則對程序文件以及程序使用和產生的默認文件都 適用(例如,"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庫的程序必須 包含它們。頭文件還用來包含數據聲明和定義,這些數據不止一個程序需要。頭文件應該按照功能組織,例如,獨立子系統的聲明應該放到獨立的頭文件 中。如果一組聲明在代碼從一種機器移植到另外一種機器時變動的可能性很大,那么這些聲明也應該被放在獨立的頭文件中。
避免私有頭文件的名字與標準庫頭文件的名字一樣。下面語句:
當預期的頭文件在當前目錄下沒有找到時,它將會包含標準庫中的math頭文件。如果這的確是你所期望發生的,那么請加上注釋。包含頭文件時不要使 用絕對路徑。當從標準位置獲取頭文件時,請使用<name>包含頭文件;或相對于當前路徑定義它們。C編譯器的"include- path"選項(在許多系統中為-l)是處理擴展私有庫頭文件的最好方法,它允許在不改變源碼文件的情況下重新組織目錄結構。
聲明了函數或外部變量的頭文件應該被那些定義了這些函數和變量的文件所包含。這樣一來,編譯器就可以做類型檢查了,并且外部聲明將總是與定義保持 一致。
在頭文件中定義變量往往是個糟糕的想法,它經常是一個在文件間對代碼進行低劣劃分的癥狀。此外,在一次編譯中,像typedef和經過初始化的數 據定義無法被編譯器看到兩次。在一些系統中,重復的沒有使用extern關鍵字修飾的未初始化定義也會導致問題。當頭文件嵌套時,會出現重復的聲 明,這將導致編譯失敗。
頭文件不應該嵌套。一個頭文件的序應該描述其使用的其他被包含的頭文件的實用特性。在極特殊情況下,當大量頭文件需要被包含在多個不同的源文件中 時,可以被接受的做法是將公共的頭文件包含在一個單獨的頭文件中。
一個通用的做法是將下面這段代碼加入到每個頭文件中以防止頭文件被意外多次包含。
我們不應該對這種避免多次包含的機制產生依賴,特別是不應該因此而嵌套包含頭文件。
2.4 其他文件
還有一個慣例就是編寫一個名為"README"的文件,用于描述程序的整體情況以及問題。例如,我們經常在README包含程序所使用的條件編譯 選項列表以及相關說明,還可以包含機器無關的文件列表等。
3. 注釋
"當代碼與注釋不一致時,兩者很可能都是錯的" -- Norm Schryer
注釋應該描述發生了什么,如何做的,參數的含義,使用和修改了哪些全局變量以及約束或Bugs。避免給那些本身很清晰的代碼加注釋,因為這些注釋信息將很快的過時。注釋與代碼不一致將會帶來負面影響。短小的注釋應該是關于做什么的,比如"計算有意義的值",而不是關于"怎么做"的,例如"值的總和除以n"。C不是匯編;在頭3-10行的區域添加注釋,說明代碼總體是做什么的,經常要比為每行添加注釋說明微邏輯更加有用。
注釋應該為那些令人不悅的代碼作出"辯護"。辯護應該是這樣的:如果使用正常的代碼,一些糟糕的事情將會發生。但僅僅讓代碼運行的更快還不足以讓這些hack代碼顯得合理化;而是應該將那些在不使用hack代碼時令人無法接受的性能數據展示出來。注釋應該對著寫不可接受的行為作出解釋,并告訴大家為什么使用Hack代碼可以很好的解決這個問題。
那些用于描述數據結構,算法等的注釋應該以塊注釋的形式存在。塊注釋起始以/*占據1-2列,*放在每行注釋前面的第二列,塊注釋最后以占據2-3列的*/結尾。另外一個候選方案是每行注釋文字前面用*占據1-2列,塊注釋以占據1-2列的*/收尾。
/*
** Alternate format for block comments
*/
將匹配文件中所有的注釋。特別長的塊注釋,諸如持久討論或版權聲明,經常以占據1-2列的/*開始,每行注釋文字前沒有*,并最終以占據1-2列的*/結束。函數內部很適合使用塊注釋,塊注釋應該與其描述的代碼擁有相同的縮進。獨立的單行注釋也應該與其說明的代碼縮進一致。
4. 聲明
全局聲明應該從第一列開始。在所有外部數據聲明的前面都應該放置extern關鍵字。如果一個外部變量是一個在定義時大小確定的數組,那么這個數 組界限必須在extern聲明時顯示指出,除非數組的大小與數組本身編碼在一起了(例如,一個總是以0結尾的只讀字符數組)。重復聲明數組大小對 于一些使用他人編寫的代碼的人特別有益。
指針修飾符*應該與變量名在一起,而不是與類型在一起。
替換
后者是錯誤的,因為實際上t和u并未如預期那樣被聲明為指針。
不相關的聲明,即使是相同類型的,也應該獨立占據一行。我們應該對聲明對象的角色進行注釋,不過當常量名本身足以說明角色時,使用#define 定義的常量列表則不需要注釋。通常多行變量名、值與注釋使用相同縮進,使得他們在一列直線上。盡量使用Tab字符而不是空格。結構體和聯合體的聲 明時,每個元素應該單獨占據一行,并附帶一條注釋。{應該與結構體的tag名放在同一行,}應該放在聲明結尾的第一列。
/* defines for boat.type */
#define KETCH (1)
#define YAWL (2)
#define SLOOP (3)
#define SQRIG (4)
#define MOTOR (5)
最重要的類型應該被typedef,即使他們只是整型,因為獨立的名字使得程序更加易讀(如果只有很少的幾個integer的typedef)。 結構體在聲明時應該被typedef。保持結構體標志的名字與typedef后的名字相同。
"C語言的觀點之一是程序員永遠是對的" ― Michael DeCorte
5. 函數聲明
每個函數前面應該放置一段塊注釋,概要描述該函數做什么以及(如果不是很清晰)如何使用該函數。重要的設計決策討論以及副作用說明也適合放在注釋 中。避免提供那些代碼本身可以清晰提供的信息。
函數的返回類型應該單獨占據一行,(可選的)縮進一個級別。不用使用默認返回類型int;如果函數沒有返回值,那么將返回類型聲明為void。如 果返回值需要大段詳細的說明,可以在函數之前的注釋中描述;否則可以在同一行中對返回類型進行注釋。函數名(以及形式參數列表)應該被單獨放在一 行,從第一列開始。目的(返回值)參數一般放在第一個參數位置(從左面開始)。所有形式參數聲明、局部聲明以及函數體中的代碼都應該縮進一級。函 數體的開始括號應該單獨一行,放在開始處的第一列。
每個參數都應該被聲明(不要使用默認類型int)。通常函數中每個變量的角色都應該被描述清楚,我們可以在函數注釋中描述,或如果每個聲明單獨一 行,我們可以將注釋放在同一行上。像循環計數器"i",字符串指針"s"以及用于標識字符的整數類型"c"這些簡單變量都無需注釋。如果一組函數 都擁有一個相似的參數或局部變量,那么在所有函數中使用同一個名字來標識這個變量是很有益處的(相反,避免在相關函數中使用一個名字標識用途不同 的變量)。不同函數中的相似參數還應該放在各個參數列表中的相同位置。
參數和局部變量的注釋應該統一縮進以排成一列。局部變量聲明應用一個空行與函數語句分隔開來。
當你使用或聲明變長參數的函數時要小心。目前在C中尚沒有真正可移植的方式處理變長參數。最好設計一個使用固定個數參數的接口。如果一定要使用變 長參數,請使用標準庫中的宏來聲明具有變長參數的函數。
如果函數使用了在文件中沒有進行全局聲明的外部變量(或函數),我們應該在函數體內部使用extern關鍵字單獨對這些變量進行聲明。
避免局部聲明覆蓋高級別的聲明。尤其是,局部變量不應該在嵌套代碼塊中被重聲明。雖然這在C中是合法的,但是當使用-h選項時,潛在的沖突可能性 足以讓lint工具發出抱怨之聲。
|
新聞熱點
疑難解答
圖片精選