本文假定讀者已具備基本的C編譯知識,如非特殊說明,文中“源檔案”指 * .c檔案,“頭檔案”指 *.h檔案,“參考”指包含頭檔案,
一、頭檔案作用
C語言里,每個源檔案是一個模塊,頭檔案為使用該模塊的用戶提供介面,介面指一個功能模塊暴露給其他模塊用以訪問具體功能的方法,
使用源檔案實作模塊的功能,使用頭檔案暴露單元的介面,用戶只需包含相應的頭檔案就可使用該頭檔案中暴露的介面,
通過頭檔案包含的方法將程式中的各功能模塊聯系起來有利于模塊化程式設計:
1)通過頭檔案呼叫庫功能,在很多場合,源代碼不便(或不準)向用戶公布,只要向用戶提供頭檔案和二進制庫即可,用戶只需按照頭檔案中的介面宣告來呼叫庫功能,而不必關心介面如何實作,編譯器會從庫中提取相應的代碼,
2)頭檔案能加強型別安全檢查,若某個介面的實作或使用方式與頭檔案中的宣告不一致,編譯器就會指出錯誤,這一簡單的規則能大大減輕程式員除錯、改錯的負擔,
在預處理階段,編譯器將源檔案包含的頭檔案內容復制到包含陳述句(#include)處,在源檔案編譯時,連同被包含進來的頭檔案內容一起編譯,生成目標檔案(.obj),
如果所包含的頭檔案非常龐大,則會嚴重降低編譯速度(使用GCC的-E選項可獲得并查看最終預處理完的檔案),因此,在源檔案中應僅包含必需的頭檔案,且盡量不要在頭檔案中包含其它頭檔案,
二、 頭檔案組織原則
源檔案中實作變數、函式的定義,并指定鏈接范圍,頭檔案中書寫外部需要使用的全域變數、函式宣告及資料型別和宏的定義,
建議組織頭檔案內容時遵循以下原則:
1)頭檔案劃分原則:型別定義、宏定義盡量與函式宣告相分離,分別位于不同的頭檔案中,內部函式宣告頭檔案與外部函式宣告頭檔案相分離,內部型別定義頭檔案與外部型別定義頭檔案相分離,
注意,型別和宏定義有時無法分拆為不同檔案,比如結構體內陣列成員的元素個數用常量宏表示時,因此僅分離型別宏定義與函式宣告,且分別置于*.th和*.fh檔案(并非強制要求),
2)頭檔案的語意層次化原則:頭檔案需要有語意層次,不同語意層次的型別定義不要放在一個頭檔案中,不同層次的函式宣告不要放在一個頭檔案中,
3)頭檔案的語意相關性原則:同一頭檔案中出現的型別定義、函式宣告應該是語意相關的、有內部邏輯關系的,避免將無關的定義和宣告放在一個頭檔案中,
4)頭檔案名應盡量與實作功能的源檔案相同,即module.c和module.h,但源檔案不一定要包含其同名的頭檔案,
5)頭檔案中不應包含本地資料,以降低模塊間耦合度,
即只有源檔案自己使用的型別、宏定義和變數、函式宣告,不應出現在頭檔案里,作用域限于單檔案的私有變數和函式應宣告為static,以防止外部呼叫,將私有型別置于源檔案中,會提高聚合度,并減少不必要的格式外漏,
6)頭檔案內不允許定義變數和函式,只能有宏、型別(typedef/struct/union/enum等)及變數和函式的宣告,
特殊情況下可extern基本型別的全域變數,源檔案通過包含該頭檔案訪問全域變數,但頭檔案內不應extern自定義型別(如結構體)的全域變數,否則將迫使本不需要訪問該變數的源檔案包含自定義型別所在頭檔案[1],
7)說明性頭檔案不需要有對應的源檔案,此類頭檔案內大多包含大量概念性宏定義或列舉型別定義,不包含任何其他型別定義和變數或函式宣告,此類頭檔案也不應包含任何其他頭檔案,
8)使用#pragma once或header guard(亦稱include guard或macro guard)避免頭檔案重復包含,#pragma once是一種非標準但已被現代編譯器廣泛支持的技巧,它明確告知前處理器“不要重復包含當前頭檔案”,而header guard則通過預處理命令模擬類似行為:

使用#pragma once相比header guard具有兩個優點:
① 更快,編譯器不會第二次讀取標記#pragma once的檔案,但卻會讀若干遍使用header guard 的檔案(尋找#endif);
② 更簡單,不再需要為每個檔案的header guard取名,避免宏名重名引發的“找不到宣告”問題,
缺點則是:
#pragma once保證物理上的同一個檔案不會被包含多次,無法對頭檔案中的一段代碼作#pragma once宣告,若某個頭檔案具有多份拷貝(內容相同的多個檔案),pragma不能保證它們不被重復包含,當然,這種重復包含很容易被發現并修正,
9) C++中要參考C函式時,函式所在頭檔案內應包含extern "C",

被extern "C"修飾的變數和函式將按照C語言方式編譯和連接,否則編譯器將無法找到C函式定義,從而導致鏈接失敗,
10)頭檔案內要有面向用戶的充足注釋,從應用角度描述介面暴露的內容,
三、 頭檔案包含原則
在實際編程中,常常因頭檔案包含不當而引發編譯時報告符號未定義的錯誤或重復定義的警告,
要消除符號未定義的編譯錯誤,只需在參考符號(變數、函式、資料型別及宏等)前確保它已被宣告或定義[4],要消除重復定義的警告,則需合理設計頭檔案包含順序和層次,
建議包含頭檔案時遵循以下原則:
1)源檔案內的頭檔案包含順序應從最特殊到一般,
如:

優點是每個頭檔案必須include需要的關聯頭檔案,否則會報錯,
同時,源檔案同名頭檔案置于包含串列前端便于檢查該頭檔案是否自完備,以及型別或函式宣告是否與標準庫沖突,
2)減少頭檔案的嵌套和交叉參考,頭檔案僅包含其真正需要顯式包含的頭檔案,
例如,頭檔案A中出現的型別定義在頭檔案B中,則頭檔案A應包含頭檔案B,除此以外的其他頭檔案不允許包含,
頭檔案的嵌套和交叉參考會使程式組織結構和檔案組織變得混亂,同時造成潛在的錯誤,大型工程中,原有頭檔案可能會被多個其他(源或頭)檔案包含,在原有頭檔案中添加新的頭檔案往往牽一發而動全身,若頭檔案中型別定義需要其他頭檔案時,可將其提出來單獨形成一個全域頭檔案,
3)頭檔案應包含哪些頭檔案僅取決于自身,而非包含該頭檔案的源檔案,
例如,編譯源檔案時需要用到頭檔案B,且源檔案已包含頭檔案A,而索性將頭檔案B包含在頭檔案A中,這是錯誤的做法,
4)盡量保證用戶使用此頭檔案時,無需手動包含其他前提頭檔案,即此頭檔案內已包含前提頭檔案,
例如,面積相關操作的頭檔案Area.h內已包含關于點操作的頭檔案Point.h,則用戶包含Area.h后無需再手動包含Point.h,這樣用戶就不必了解頭檔案的內在依賴關系,
5)頭檔案應是自完備的,即在任一源檔案中包含任一頭檔案而不會產生編譯錯誤,
6)源檔案中包含的頭檔案盡量不要有順序依賴,
7)盡量在源檔案中包含頭檔案,而非在頭檔案中,且源檔案僅包含所需的頭檔案,
8)頭檔案中若能前置宣告(亦稱前向宣告[5]),就不要包含另一頭檔案,僅當前置宣告不能滿足或過于麻煩時才使用include,如此可減少依賴性方面的問題,
示例如下:

如上,在OmciChkFunc函式的實作源檔案內包含T_MeInfoMap和T_OmciMsg所在頭檔案即可,
另舉一例如下:

如上,CompareRecFunc函式原型由其他頭檔案提供,此處為避免頭檔案交叉參考定義其異名同構原型CmpRecFunc,
在不會引起歧義的前提下,頭檔案內盡可能使用VOID指標代替非基本型別的值變數或指標,以避免再包含型別定義所在的頭檔案,但這將影響代碼可讀性并降低程式執行效率,應權衡利弊,
9)避免包含重量級的平臺頭檔案,如windows.h或d3d9.h等,若僅使用該頭檔案少量函式,可extern函式到源檔案內,如下:

若還使用該頭檔案某些型別和宏定義,可創建適配性源檔案,在該源檔案內包含平臺頭檔案,封裝新的介面并將其宣告在同名頭檔案內,其他源檔案將通過適配頭檔案間接訪問平臺介面,如下:

10)對于函式庫(包括標準庫和自定義的公共宏及介面)的頭檔案,可將其加入到一個通用頭檔案中,需要控制該頭檔案的體積(主要是該頭檔案所包含的所有頭檔案內容大小),并確保所有源檔案首先包含該通用頭檔案,示例如下:

注意,示例頭檔案內包含C庫檔案雖能簡化包含,但卻與規則1沖突,也可另外增加包含庫檔案串列的通用頭檔案,
11)若不確定型別、宏定義或函式宣告所在頭檔案具體路徑,可在源檔案中再次定義或宣告,編譯器會以redefined警告或conflicting錯誤給出型別、宏定義或函式宣告所在頭檔案路徑,
四、代碼檔案組織原則
建議C語言專案中代碼檔案組織遵循以下原則:
1)使用層次化和模塊化的軟體開發模型,每個模塊只能使用所在層和下一層模塊提供的介面,
2)每個模塊的檔案(可能多個)保存在一個獨立檔案夾中,
模塊檔案較多時可采用子目錄的方式,物理上隔離不同層次的檔案,子目錄下源檔案和頭檔案應分開存放,如分別置入include和source目錄,
3)用于模塊裁減的條件編譯宏保存在一個獨立檔案中,便于軟體裁減,
4)硬體相關代碼和作業系統相關代碼與工程代碼相對獨立保存,以便于軟體移植,
5)按相同功能或相關性組織源檔案和頭檔案,同一檔案內的聚合度要高,不同檔案中的耦合度要低,
在對既有工程做單元測驗時,耦合度低的檔案布局非常便于搭建環境,
6)宣告和定義分開,使用頭檔案暴露模塊需要提供給外部的型別、宏、變數和函式,盡量做到模塊對外部透明,用戶在使用模塊功能時無需了解具體的實作,
7)作為對外介面的頭檔案一經發布,應保持穩定,修改時一定要慎重,
8)檔案夾和檔案命名要能夠反映出模塊的功能,
9)正式版本和測驗版本使用統一檔案,使用宏控制是否產生測驗輸出,
10)必要的注釋不可缺少,

最后,不管你是轉行也好,初學也罷,進階也可,如果你想學編程~
【值得關注】我的 C/C++編程學習交流俱樂部!【點擊進入】
問題答疑,學習交流,技術探討,還有超多編程資源大全,零基礎的視頻也超棒~
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/251348.html
標籤:C
上一篇:C語言中使用指標與陣列的差異
下一篇:函式指標
