模組的封裝(四):標頭檔案的疼

無痕幽雨發表於2018-02-09

出處: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. [交流][微知識]模組的封裝(三):無傷大雅的形式主義
 

相關文章