模組的封裝(四):標頭檔案的疼
出處:https://www.amobbs.com/thread-5675247-1-1.html
[交流][微知識]模組的封裝(四):標頭檔案的疼
認真說起來,標頭檔案(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 Header File)。
需要強調的是,資訊的流動方向是 從外向內,所以又可以簡單的理解為輸入性的標頭檔案(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原始檔中。
To be continue...
相關文章
a. [交流][微知識]模組的封裝(一):C語言類的封裝
b. [交流][微知識]模組的封裝(二):C語言類的繼承和派生
c. [交流][微知識]模組的封裝(三):無傷大雅的形式主義
相關文章
- 標頭檔案的作用分析
- vue專案的網路模組封裝Vue封裝
- 8.13 標頭檔案剖析:標頭檔案路徑(下)
- 教程中 令人頭疼的 前端流安裝前端
- 02@在類的標頭檔案中儘量少引入其他標頭檔案
- C 標頭檔案
- 關於C++的標頭檔案C++
- #include sys/xxx.h標頭檔案 UNIX標頭檔案
- locate標頭檔案和庫檔案
- C 標頭檔案 作用
- 祖傳標頭檔案
- 標頭檔案講解
- algorithm標頭檔案下的常用函式Go函式
- C語言標頭檔案#include的作用C語言
- javaseverlet實現的http標頭檔案的讀取JavaHTTP
- fcntl.h標頭檔案
- linux 標頭檔案 作用Linux
- 什麼是 標頭檔案
- 8.14 Linux核心中的標頭檔案Linux
- C++ 標頭檔案的包含順序研究C++
- Linux一些重要的標頭檔案Linux
- C語言標頭檔案的使用(轉載)C語言
- Opencv各個版本的萬能標頭檔案OpenCV
- C語言關於標頭檔案的使用C語言
- wav檔案的檔案頭
- JavaScript 模組封裝JavaScript封裝
- 頭疼的null值,自敬彬Null
- 日誌模組(一標頭檔案就實現了日誌記錄)
- C語言中的標頭檔案中的巨集定義C語言
- C 語言的標頭檔案是必須的嗎
- 關於QT的標頭檔案相互包含的問題QT
- C/C++標頭檔案太難記?一個萬能標頭檔案全搞定!C++
- 介紹下extern和標頭檔案的聯絡
- mac CLion cmake 呼叫自己定義的標頭檔案Mac
- C 語言標頭檔案作用的簡單理解
- openGauss libpq使用依賴的標頭檔案
- c++筆記_標頭檔案C++筆記
- Nt函式原型標頭檔案函式原型