標籤:

c++程序哪些應該放在頭文件裡面,哪些應該放在源文件裡面?

如果我需要實現一個類,我把它放在頭文件裡面了。這個類需要一些其他的類,枚舉類型之類的東西,那麼應該如何處理這些模塊的放置呢?


只要不會引起編譯錯誤,並且不會要求你要使用前置聲明的話,那盡量放在cpp裡面總是不會錯的。只有很少數的情況下你需要考慮要不要放進頭文件里,譬如說inline函數,或者static const int變數等等。


TL;DR

這個問題相當寬泛,答案也可以引申出很多值得尋味的細節。我簡單歸納一下:

總體來說,取決你項目的關注點和你的coding style:

  1. 一般情況下,盡量把能放到源文件cpp的代碼都放到源文件里。這一點待會詳細說。

  2. 特殊情況下,比如完全template的header-only generic code,可以考慮建兩個頭文件。聲明(declarations)放到第一個頭文件,實現(implementation detail)放到第二個頭文件里:前者#include後者。

  • 先說第一點:盡量把能放到cpp的code放到cpp里。

先從編譯器角度說一點,頭文件中聲明的static變數等,要記得在源文件中定義。如果有頭文件中直接定義的函數,記得mark inline,否則如果有多個translation unit同時引用時,會產生multiple definitions的錯誤(違背了One Definition Rule)。

下面從軟體庫開發的角度,講一下自己的理解。

很多時候,對於你的項目,庫,或者產品,encapsulation是極為重要的一點。從這個方面考慮,頭文件中應該只能有public APIs,其他所有的屬於implementation detail的東西都應當放在源文件裡面。一個項目的遞交經常是public APIs + compiled library (for linking)。

這樣的話,有若干優點

從開發者的角度講:

1。保護源代碼(正所謂encapsulation)

2。不必要的信息沒有必要展示給用戶

從用戶的角度講:

1。用戶大多數情況下僅需查看API,頭文件摻雜實現代碼會顯得繁瑣不宜查詢。

2。很重要的一點:當開發者更新庫的實現後,用戶只需更新,然後重新link即可,不必重新編譯

從產品或者庫的質量角度講:

頭文件,源文件區分得當的庫,編譯時間+運行程序體積(binary size)會比純粹頭文件的庫優化很多。這一點有時也很重要:

1。設想用你的庫開發的項目,每次更改都需費長時間編譯,對開發效率和應用體驗都不好;

2。特別對於嵌入式項目,運行程序體積往往有一定限制,太大的binary甚至很可能不會cache friendly,影響運行效率。

綜上,原則上應該把頭文件和源文件盡量區分開。從encapsulation方面講:源代碼保護,API清晰化;從編譯時間+開發使用講:減少無用的重新編譯,方便用戶更新(這也是為何pimpl idiom被廣泛使用的一個原因,reference:The Joy of Pimpls (or, More About the Compiler-Firewall Idiom))

那麼inline或者template怎麼辦?

template

從編譯器角度考慮,template的實現似乎只能放在同文件中,放在cpp中會出現linker error。可是還是有技巧可以將template的實現放到cpp中:在源文件對template argument類型完全特化(explicit specialization,具體見:Standard C++)。

- 這一點,如果從開發庫的廣泛型(genericity)角度考慮,可能完全不合邏輯;但是對於交付給用戶應用程序的特殊庫版本,這麼做是會有一定收穫的。具體來講,如果對於用戶應用程序,數據類型是固定的,那麼可以完全提供一個庫版本,其template argument在cpp中完全specialize化:這樣對於應用程序的角度,收穫就是前面提到的幾點。

-對於庫的開發者,template的實現必然要放在頭文件中,但是,還是有技巧將實現分離於用戶的public APIs的:詳見第二點。

inline

inline函數(非static)若果定義不在頭文件中,會產生linker error。一個特例是,如果確定此函數只會被一個translation unit引用,則可以將其放到cpp中,不過這種情況較少見,也不推薦。同template一樣,有同樣的技巧,可以將inline函數的實現分離於用戶的public APIs:詳見第二點。

  • 再說第二點:header中隱藏implementation detail

很多header only library注重運行時間的效率(runtime efficiency),泛化(genericity),因此廣泛使用template,並給用戶對參數類型選擇的靈活性。在這種template library裡面,可以用多個頭文件的方式將代碼實現和APIs分開。

具體的做法就是,創建一個頭文件(比如 xx.hh),僅包含API;再用另一個頭文件封裝代碼實現(比如 xx.hxx)。前者因用後者,就實現了頭文件源文件的分離。這種做法在STL,boost等庫里都是很常見的。開發者通常將doc寫在第一個頭文件中,第二個頭文件被當作源代碼,用doxygen產生doc時濾掉第二個頭文件的後綴名,這樣只有第一個頭文件的public APIs進入documentation,從而產生清晰可讀的文檔。

同樣的方法也適用於inline函數。

如有分析不到的地方,還請大家指正!


聲明放頭文件,定義放在.cpp文件。這裡的定義是指編譯後,在內存的數據和代碼的程序元素。

比如struct的定義本質上是聲明,放在·h文件,但需要控制只include一次。否則編譯時,重定義報錯。

Inline函數定義需要放在頭文件中。

模板定義放在頭文件中

頭文件也可以包含static修飾變數定義和函數定義


我一般是把每一個cpp文件視作一個模塊,頭文件視作模塊導出的介面(其他模塊會用到)。所以頭文件裡面一般就只有類的聲明,函數的聲明,模板及其實現,以及需要給其他模塊用的變數(extern)。這樣的做法,像是visitor模式那類互相依賴的情況也正好解決了。


除了公用的部分放在頭文件,其他能放在cpp裡面的就不要考慮h文件


習慣方法是:定義放在頭文件,實現放在cpp。而頭文件又可分為公用頭文件和私用頭文件, @Jon Lee 總結的比較全面,這裡不多講


個人比較傾向於頭文件裡面盡量不include其他頭文件,而是在cpp裡面去include,盡量減少頭文件被間接包含。

比如A類要用到B類,那麼可以在A.h中class B;然後A申明內部用B*,然後在A.cpp中再去include B.h。

其他像枚舉這樣的就沒辦法了,只能在頭文件include


推薦閱讀:

編譯器生成的彙編語句執行順序為什麼與C代碼順序不同?
編譯器能否對如下場景優化,以及如何檢查不同編譯器對此是否做了優化?
C++如何判斷一個整數溢出?
c語言中一個函數的聲明和定義有區別嗎?
C++中生成隨機數的問題?

TAG:C | CC |