通常每一個 `.cc` 文件都有一個對應的 `.h` 文件. 也有一些常見例外, 如單元測試代碼和只包含 `main()` 函數的 `.cc` 文件.
正確使用頭文件可令代碼在可讀性、文件大小和性能上大為改觀.
下面的規則將引導你規避使用頭文件時的各種陷阱.
## 1.1. #define 保護
> Tip
> 所有頭文件都應該使用 `#define` 防止頭文件被多重包含, 命名格式當是: `<PROJECT>_<PATH>_<FILE>_H_`
為保證唯一性, 頭文件的命名應該依據所在項目源代碼樹的全路徑. 例如, 項目 `foo` 中的頭文件 `foo/src/bar/baz.h` 可按如下方式保護:
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
…
#endif // FOO_BAR_BAZ_H_
## 1.2. 頭文件依賴
> Tip
> 能用前置聲明的地方盡量不使用 #include.
當一個頭文件被包含的同時也引入了新的依賴, 一旦該頭文件被修改, 代碼就會被重新編譯. 如果這個頭文件又包含了其他頭文件, 這些頭文件的任何改變都將導致所有包含了該頭文件的代碼被重新編譯. 因此, 我們傾向于減少包含頭文件, 尤其是在頭文件中包含頭文件.
使用前置聲明可以顯著減少需要包含的頭文件數量. 舉例說明: 如果頭文件中用到類?`File`, 但不需要訪問?`File`?類的聲明, 頭文件中只需前置聲明?`class?File;`?而無須?`#include"file/base/file.h"`.
不允許訪問類的定義的前提下, 我們在一個頭文件中能對類?`Foo`?做哪些操作?
* 我們可以將數據成員類型聲明為?`Foo?*`?或?`Foo?&`.
* 我們可以將函數參數 / 返回值的類型聲明為?`Foo`?(但不能定義實現).
* 我們可以將靜態數據成員的類型聲明為?`Foo`, 因為靜態數據成員的定義在類定義之外.
反之, 如果你的類是?`Foo`?的子類, 或者含有類型為?`Foo`?的非靜態數據成員, 則必須包含?`Foo`?所在的頭文件.
有時, 使用指針成員 (如果是?`scoped_ptr`?更好) 替代對象成員的確是明智之選. 然而, 這會降低代碼可讀性及執行效率, 因此如果僅僅為了少包含頭文件,還是不要這么做的好.
當然?`.cc`?文件無論如何都需要所使用類的定義部分, 自然也就會包含若干頭文件.
## 1.3. 內聯函數
> Tip
> 只有當函數只有 10 行甚至更少時才將其定義為內聯函數.
定義:
當函數被聲明為內聯函數之后, 編譯器會將其內聯展開, 而不是按通常的函數調用機制進行調用.
優點:
當函數體比較小的時候, 內聯該函數可以令目標代碼更加高效. 對于存取函數以及其它函數體比較短, 性能關鍵的函數, 鼓勵使用內聯.
缺點:
濫用內聯將導致程序變慢. 內聯可能使目標代碼量或增或減, 這取決于內聯函數的大小. 內聯非常短小的存取函數通常會減少代碼大小, 但內聯一個相當大的函數將戲劇性的增加代碼大小. 現代處理器由于更好的利用了指令緩存, 小巧的代碼往往執行更快。
結論:
一個較為合理的經驗準則是, 不要內聯超過 10 行的函數. 謹慎對待析構函數, 析構函數往往比其表面看起來要更長, 因為有隱含的成員和基類析構函數被調用!
另一個實用的經驗準則: 內聯那些包含循環或?`switch`?語句的函數常常是得不償失 (除非在大多數情況下, 這些循環或?`switch`?語句從不被執行).
有些函數即使聲明為內聯的也不一定會被編譯器內聯, 這點很重要; 比如虛函數和遞歸函數就不會被正常內聯. 通常, 遞歸函數不應該聲明成內聯函數.(YuleFox 注: 遞歸調用堆棧的展開并不像循環那么簡單, 比如遞歸層數在編譯時可能是未知的, 大多數編譯器都不支持內聯遞歸函數). 虛函數內聯的主要原因則是想把它的函數體放在類定義內, 為了圖個方便, 抑或是當作文檔描述其行為, 比如精短的存取函數.
## 1.4. -inl.h文件
> Tip
> 復雜的內聯函數的定義, 應放在后綴名為?`-inl.h`?的頭文件中.
內聯函數的定義必須放在頭文件中, 編譯器才能在調用點內聯展開定義. 然而, 實現代碼理論上應該放在?`.cc`?文件中, 我們不希望?`.h`?文件中有太多實現代碼, 除非在可讀性和性能上有明顯優勢.
如果內聯函數的定義比較短小, 邏輯比較簡單, 實現代碼放在?`.h`?文件里沒有任何問題. 比如, 存取函數的實現理所當然都應該放在類定義內. 出于編寫者和調用者的方便, 較復雜的內聯函數也可以放到?`.h`?文件中, 如果你覺得這樣會使頭文件顯得笨重, 也可以把它萃取到單獨的?`-inl.h`?中. 這樣把實現和類定義分離開來, 當需要時包含對應的?`-inl.h`?即可。
`-inl.h`?文件還可用于函數模板的定義. 從而增強模板定義的可讀性.
別忘了?`-inl.h`?和其他頭文件一樣, 也需要?`#define`?保護.
## 1.5. 函數參數的順序
> Tip
> 定義函數時, 參數順序依次為: 輸入參數, 然后是輸出參數.
C/C++ 函數參數分為輸入參數, 輸出參數, 和輸入/輸出參數三種. 輸入參數一般傳值或傳?`const`?引用, 輸出參數或輸入/輸出參數則是非-`const`?指針. 對參數排序時, 將只輸入的參數放在所有輸出參數之前. 尤其是不要僅僅因為是新加的參數, 就把它放在最后; 即使是新加的只輸入參數也要放在輸出參數之前.
這條規則并不需要嚴格遵守. 輸入/輸出兩用參數 (通常是類/結構體變量) 把事情變得復雜, 為保持和相關函數的一致性, 你有時不得不有所變通.
## 1.6.?`#include`?的路徑及順序
> Tip
> 使用標準的頭文件包含順序可增強可讀性, 避免隱藏依賴: C 庫, C++ 庫, 其他庫的?.h, 本項目內的?.h.
項目內頭文件應按照項目源代碼目錄樹結構排列, 避免使用 UNIX 特殊的快捷目錄?`.`?(當前目錄) 或?`..`?(上級目錄). 例如,?`google-awesome-project/src/base/logging.h`?應該按如下方式包含:
~~~
#include "base/logging.h"
~~~
又如,?`dir/foo.cc`?的主要作用是實現或測試?`dir2/foo2.h`?的功能,?`foo.cc`?中包含頭文件的次序如下:
1. `dir2/foo2.h`?(優先位置, 詳情如下)
2. C 系統文件
3. C++ 系統文件
4. 其他庫的?`.h`?文件
5. 本項目內?`.h`?文件
這種排序方式可有效減少隱藏依賴. 我們希望每一個頭文件都是可被獨立編譯的 (yospaly 譯注: 即該頭文件本身已包含所有必要的顯式依賴), 最簡單的方法是將其作為第一個?`.h`?文件?`#included`?進對應的?`.cc`.
`dir/foo.cc`?和?`dir2/foo2.h`?通常位于同一目錄下 (如?`base/basictypes_unittest.cc`?和?`base/basictypes.h`), 但也可以放在不同目錄下.
按字母順序對頭文件包含進行二次排序是不錯的主意 (yospaly 譯注: 之前已經按頭文件類別排過序了).
舉例來說,?`google-awesome-project/src/foo/internal/fooserver.cc`?的包含次序如下:
~~~
#include "foo/public/fooserver.h" // 優先位置
#include
#include
#include
#include
#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"
~~~
## 譯者 (YuleFox) 筆記
1. 避免多重包含是學編程時最基本的要求;
2. 前置聲明是為了降低編譯依賴,防止修改一個頭文件引發多米諾效應;
3. 內聯函數的合理使用可提高代碼執行效率;
4. `-inl.h`?可提高代碼可讀性 (一般用不到吧:D);
5. 標準化函數參數順序可以提高可讀性和易維護性 (對函數參數的堆棧空間有輕微影響, 我以前大多是相同類型放在一起);
6. 包含文件的名稱使用?`.`?和?`..`?雖然方便卻易混亂, 使用比較完整的項目路徑看上去很清晰, 很條理, 包含文件的次序除了美觀之外, 最重要的是可以減少隱藏依賴, 使每個頭文件在 “最需要編譯” (對應源文件處 :D) 的地方編譯, 有人提出庫文件放在最后, 這樣出錯先是項目內的文件, 頭文件都放在對應源文件的最前面, 這一點足以保證內部錯誤的及時發現了.
- Google 開源項目風格指南 (中文版)
- C++ 風格指南
- 0. 扉頁
- 1. 頭文件
- 2. 作用域
- 3. 類
- 4. 來自 Google 的奇技
- 5. 其他 C++ 特性
- 6. 命名約定
- 7. 注釋
- 8. 格式
- 9. 規則特例
- 10. 結束語
- Objective-C 風格指南
- Google Objective-C Style Guide 中文版
- 留白和格式
- 命名
- 注釋
- Cocoa 和 Objective-C 特性
- Cocoa 模式
- Python 風格指南
- Google Python 風格指南 - 中文版
- 背景
- Python語言規范
- Python風格規范
- 臨別贈言
- JSON 風格指南
- 簡介
- 定義
- 一般準則
- 屬性名準則
- 屬性值準則
- 屬性值數據類型
- JSON結構和保留屬性名
- 頂級保留屬性名稱
- data對象的保留屬性名
- 用于分頁的保留屬性名
- 用于鏈接的保留屬性名
- 錯誤對象中的保留屬性名
- 屬性順序
- 示例
- 附錄