認真說起來,頭文件(Header File)是個短命的家伙——就整個編譯過程來說,它的壽命是最短的。
為什么這么說呢?關于頭文件的話題,討論起來那可是“孩子沒娘,說來話長了”,既然是閑聊、你也不
是等著這篇文章救命,那就不妨從頭開始說起——先假設讀者們都是不了解編譯基本過程的初學者。
一個編譯(Compilation)過程通常至少分為三個階段:預編譯(Precompiling)、編譯(Make)和鏈接
(Linking)。他們就像一個流水線一環套一環——前一工序的輸出是后一工序的輸入。這本沒有什么稀奇的,
但對于程序員來說,這個過程中有幾個基本常識是需要記住的:
1. C語言編譯的基本單位(Compilation Unit)是 C源文件(而并沒有頭文件)
2. 同一個工程中,不同C源文件的編譯是彼此獨立的(毫不相干的)
3. 頭文件在預編譯階段就已經合并到對應的C源文件中了,和所有的宏以及條件編譯一樣,到了編譯階段,所有的頭文件、宏都是不存在的,已經被替換為對應的內容和常量了。
理解這三點,基本上已經可以解決很多我們日常編碼過程中存在的很多疑問,比如:
- Q1:為什么不能C語言頭文件里面定義變量或者函數的實體?
- Q2:為什么有的時候宏的先后順序并不那么重要?
- Q3:為什么可以在源代碼的任意位置(另起一行后)定義宏,甚至是include別的頭文件?
推薦大家基于前面的三個事實自己思考,答案在附錄中介紹。
頭文件里可以放什么呢?這是個值得討論的問題:
- 各類宏
- 函數的聲明(也就是 extern xxxxx)
- 全局變量的聲明(也就是 extern xxxx)
然而,值得說明的是,這里有一個編碼規則值得你去遵守:頭文件里堅決不要放全局變量有關的任何東西(硬要加,也必須是const類型的,比如各類接口)。
- 類型定義(typedef, struct,union 之類的)
- static 的變量實體和函數實體。
這個可以有,為啥呢?因為即便多個c源文件包含同一個頭文件導致同樣的函數和變量實體存在多份,但
static 的另外一個名字 "private" 可以保證每一份變量和函數實體都是彼此獨立的,都是每個c源代碼的
私人財產——你可以有,我也可以有!鞍?你也有啊,真巧哎,我也有……”
- inline 的函數
這個和static是一個道理。
頭文件里面不能放函數的實體,想必原因大部分人都知道了,這里就不再贅述。但頭文件里不放(非const)的全局變量的聲明,
這怎么玩?這里需要說明一下,頭文件里不是不能放(非const)的全局變量聲明,而是我提供了一個人為的規定(規范),建議
不要放任何(非const)的全局變量到頭文件里,具體原因和解決方案,我們在別的帖子里再討論(其實有人討論過,大約就是,
如何避免使用全局變量)——是的,避免使用(非const)的全局變量是可以做到的——這里也不再贅述。說了這么多廢話,我們
真正要討論的內容還沒有開始:
- 如何建立頭文件的使用規則,使其即靈活、使用方便,又靈活且便于擴展(模塊化)——符合面向接口開發的要求,方便我們
建立黑盒子?
簡而言之,
- 如何讓頭文件的使用不再頭疼;永遠告別循環包含;方便代碼的移植?
首先,思考一個簡單的問題?為什么我們要用頭文件?答案其實很簡單,因為每個.c文件都是獨立編譯的,因此需要在源代碼
級別傳遞一些信息,類似一群人在嘮嗑:
源代碼A: 我定義了一個函數,你們哥幾個要用么?
源代碼B和源代碼C: 我們要用啊,函數原型(prototype)什么樣子啊?
源代碼A: 你們不用費腦經記(抄下來),我都寫好了,放在一個頭文件里了,你們直接include就可以了。
源代碼B和源代碼C: 這個敢情方便。那你頭文件放哪里了?
源代碼A: 有兩種方式,要么你直接到我這里來拿(指定路徑);要么你找編譯器問(編譯器指定搜索路徑)。
源代碼D: 你們整這么麻煩做什么?你直接告訴我原型,我抄下來,不就不用問這個問那個,還包含文件什么的,真麻煩。
源代碼A: D啊,你老想耍小聰明,萬一我更新了你不知道怎么辦?我有義務告訴你么?并沒有。
源代碼B和源代碼C: 是啊,是啊,A以后估計要外包了,不在這里了,到時候有變化,都記錄在頭文件里,你本地放一個,沒法
及時同步的。
源代碼D: 我不聽!我不聽!我不聽……
是不是很有畫面感?拋開捂著耳朵的D,我們回到討論的話題——既然頭文件是用來交換信息的,那么如果把所有的信息都放在一起,大家
需要的時候各取所需,豈不美哉?——基于這種思想,幾乎所有人都見過把所有變量、函數、宏、類型定義都放到一個叫做system.h的頭文件
里的做法。你有這么做過么?不要不好意思,幾乎所有人都這么做過——因為實在太方便了,世界大同,挺好,直到你嘗試和別人一起合作開發
系統,并試圖在不同項目間復用一些代碼的時候:
“何首烏藤和木蓮藤纏絡著”……對于這種情況,我們叫做耦合。“是要找個時間來理一理了”,你對自己說,然后長嘆了一口氣,發現這句話其
實很早之前就說過了。想到還有更奇葩的循環包涵的問題,你不得不感嘆,頭文件真的是個頭疼的東西——要不我們還是不用了吧?直接抄下來
貌似更簡單啊——源程序D癡癡的笑了。
那么,如何解決這個問題呢?其實,從實踐經驗來看,頭文件的用途分為兩大類:
站在C源文件的視角上:
- 從 外部向C源文件內部 輸入配置信息——我們把這類頭文件叫做配置頭文件(Configuration HeaderFile)。
需要強調的是,信息的流動方向是 從外向內,所以又可以簡單的理解為輸入性的頭文件(Header File for information input)。常見的app_cfg.h
就是典型的配置頭文件。
- 從 C源文件內部向外 輸出接口信息(全局函數、類型,宏定義等信息)——我們把這類頭文件叫做接口頭文件(Interface Header File)。
需要強調的是,信息的流動方向是 從內向外,所以又可以簡單的理解為輸出性的頭文件(Header File for information output)。常見的, spi.h
usart.h, device.h, stdint.h 就是典型的接口頭文件。
輸入和輸出兩個不同的職能如果被放在同一個頭文件里,就有極大的風險產生循環包含(兩個相反方向的箭頭產生閉合的圓圈)。system.h實際
上就是一個混淆信息流動方向的例子。這就是本質上依賴system.h的工程 模塊不好拆分的原因。根據上述原理,這里引入頭文件使用的第一條原則:
對一個C源代碼來說,站在它的視角上,隸屬于它自己的接口頭文件(Output)和配置頭文件(Input)永遠不要同時包含(include)在當前
的C源文件中。
全部資料51hei下載地址:
頭文件編寫.docx
(332.33 KB, 下載次數: 52)
2019-6-15 22:45 上傳
點擊文件名下載附件
下載積分: 黑幣 -5
|