天天看點

C/C++中的源檔案與頭檔案的差別

問題提出的背景:最近在自己動手,用C來實作各類經典算法,還搬到了Github上,但是有一個問題比較困擾我,就是這些可以複用的,作為工具方法的算法,究竟應該放在頭檔案還是源檔案裡?一般的、通用的準則到底是什麼呢?或者說頭檔案與源檔案的作用究竟是什麼?在編譯連接配接等過程中,編譯器會對他們有怎樣的差別對待呢?

一、實作究竟放在哪裡?

首先回答第一個問題,一般來說,什麼時候需要把實作放在頭檔案裡,什麼時候又需要把實作放在源檔案裡?

(此部分參考:http://www.cnblogs.com/moodlxs/archive/2012/03/08/2385118.html)

不把實作放在頭檔案中,往往是出于以下幾種顧慮:

1、暴露了實作細節

2、頭檔案被包含到不同的源檔案中,會導緻連結沖突

3、頭檔案被包含到不同的源檔案中,會導緻有多份實作被編譯出來,增大可執行體的體積

如果有顧慮 1 ,那很顯然應該在第一時間抛棄完全在頭檔案中實作的念頭。

至于顧慮 2和3 的,我們舉例如下。例如有以下頭檔案 c_function.h:

int integer_add(const int a, const int b)

{

         return a + b;

}

如果在同一工程中,有 a.c 和 b.c 兩個(或兩個以上)源檔案包含了此頭檔案,則在連結時期就會發生沖突,因為在兩個源檔案編譯得到的目标檔案中都有一份 integer_add 的函數實作,導緻連結器不知道對于調用了此函數的調用者,應該使用哪一個副本。解決沖突辦法有兩個,一個是加上 inline ,另一個是加上 static 。使用這兩個關鍵字的任意一個來修飾 integer_add 函數,然而本質卻大不相同。

如果使用 inline ,則意味着編譯器會在調用此函數的地方把函數的目标代碼直接插入,而不是放置一個真正的函數調用,實際作用就是這個函數事實上已經不再存在,而是像宏一樣被就地展開了。使用 inline 的副作用,首先在于毋庸置疑地,代碼的體積變大了;其次則是,這個關鍵字嚴格算起來并不是 C 語言的關鍵字,使用它多少會帶來一些移植性方面的風險。而且inline不對編譯器做強制要求,編譯器有權把它實作為非inline的狀态(可能的原因有,函數太大或者複雜度過高)。這樣的後果是不确定的。

如果是使用static,那麼包含此頭檔案的源檔案中都會存在此函數的一份副本。因為 static 關鍵字保證了該函數為單個源檔案之内可見,是以不會産生沖突問題。雖然代碼也有一定程度的膨脹,但至少結果是可預料的。

另外,應該避免使用extern關鍵字,如果在兩個檔案中重複定義了一個函數,并且在其中一個檔案中對這個函數使用了extern關鍵字進行修飾,那麼就會發生連接配接錯誤。

是以把實作放在頭檔案裡,似乎不是一個很好的辦法,但并不是不能這麼做。

雖然這些讨論主要聚焦在 C 語言上,但由于 C++ 是 C 語言的超集,并且在這些方面并沒有做太多的修改,是以讨論結果同樣也适用于 C++ 。

二、源檔案與頭檔案的關系

接下來,我們談一下頭檔案和源檔案在編譯與組建的過程中的關系。

編譯器就将源檔案(.cpp)編譯成目标檔案(.obj),目标檔案就是編譯單元。一個程式可以由一個編譯單元組成,也可以有多個編譯單元組成。一個函數不能放到兩個編譯單元裡面,但兩個函數或以上就可以分别放在一個單元裡面。那麼就是一個源檔案對應一個目标檔案,然後通過連結器組成一個.exe,也就是程式了。

在C++中,使用函數或者變量之前必須要進行聲明。那麼如果一個源檔案要用到另一個源檔案定義的函數,隻需在這個源檔案中寫上他的函數聲明就可以了,其餘工作由連結器幫你完成。但是當多個檔案都需要使用同一個函數時,那麼就要在多份源檔案中進行聲明。而且如果要修改這個函數時,就必須逐個修改每個源檔案。

頭檔案(.h)就是為了解決這個問題而誕生,他包含了這些公共的函數定義,而且如果需要修改,也隻修改頭檔案中的内容即可。對于商業C++程式庫,一般把頭檔案随二進制的庫檔案釋出,而把源代碼保留,這也是上面所說的顧慮1。

所有需要使用該函數的源檔案隻需要用#include語句将相應的頭檔案包含進去便可。預處理器發現#include指令後,就會尋找指令後面的檔案名并把這個檔案的内容包含到目前檔案中。被包含檔案中的文本将替換源代碼檔案中的#include指令,就像你把被包含檔案中的全部内容鍵入到源檔案中的這個位置一樣。頭檔案是沒有編譯意義的,編譯器隻編譯源檔案生成目标檔案,而頭檔案不參與編譯過程。

另外,使用#include指令包含源檔案也是可行的,編譯器完全能夠正常處理,甚至可以使用#include指令包含任意擴充名的檔案。是以從設計角度上講,源代碼區分為.h和.c,僅僅是為了接口與實作的分離,實際上兩者沒什麼本質的差别。頭檔案隻是工具,但不是必須的。

三、在這個場景下的結論

經過以上的讨論,最終的結果是,似乎在開源的工具方法中,把實作放在頭檔案裡是個非常不錯的選擇。主要的有點有三個:

1、沒有隐藏實作細節的要求,因為這是一份開源的、用作學習與練習的代碼。

2、頭檔案不參與編譯,再單獨使用衛兵宏(#ifndef... #define... #endif),就可以避免多個檔案引用本工具方法時,可能帶來的重複編譯(即連結錯誤,比如說LNK2005)。

3、不需要像商業程式庫一樣,把實作部分單獨打包生成lib或dll等二進制檔案,是以代碼體積小,結構也簡單。

轉載于:https://www.cnblogs.com/superpig0501/p/3967578.html