Google C++ 程式設計風格指南:標頭檔案

readthedocs發表於2017-02-03

通常每一個 .cc 檔案都有一個對應的 .h 檔案. 也有一些常見例外, 如單元測試程式碼和只包含 main() 函式的 .cc 檔案.

正確使用標頭檔案可令程式碼在可讀性、檔案大小和效能上大為改觀.

下面的規則將引導你規避使用標頭檔案時的各種陷阱.

1.1. Self-contained 標頭檔案

標頭檔案應該能夠自給自足(self-contained,也就是可以作為第一個標頭檔案被引入),以 .h 結尾。至於用來插入文字的檔案,說到底它們並不是標頭檔案,所以應以 .inc 結尾。不允許分離出 -inl.h 標頭檔案的做法.

所有標頭檔案要能夠自給自足。換言之,使用者和重構工具不需要為特別場合而包含額外的標頭檔案。詳言之,一個標頭檔案要有 1.2. #define 保護,統統包含它所需要的其它標頭檔案,也不要求定義任何特別 symbols.

不過有一個例外,即一個檔案並不是 self-contained 的,而是作為文字插入到程式碼某處。或者,檔案內容實際上是其它標頭檔案的特定平臺(platform-specific)擴充套件部分。這些檔案就要用 .inc 副檔名。

如果 .h 檔案宣告瞭一個模板或行內函數,同時也在該檔案加以定義。凡是有用到這些的 .cc 檔案,就得統統包含該標頭檔案,否則程式可能會在構建中連結失敗。不要把這些定義放到分離的 -inl.h 檔案裡(譯者注:過去該規範曾提倡把定義放到 -inl.h 裡過)。

有個例外:如果某函式模板為所有相關模板引數顯式例項化,或本身就是某類的一個私有成員,那麼它就只能定義在例項化該模板的 .cc 檔案裡。

1.2. #define 保護

所有標頭檔案都應該使用 #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.3. 前置宣告

儘可能地避免使用前置宣告。使用 #include 包含需要的標頭檔案即可。

定義:

所謂「前置宣告」(forward declaration)是類、函式和模板的純粹宣告,沒伴隨著其定義.

優點:

  • 前置宣告能夠節省編譯時間,多餘的 #include 會迫使編譯器展開更多的檔案,處理更多的輸入。
  • 前置宣告能夠節省不必要的重新編譯的時間。 #include 使程式碼因為標頭檔案中無關的改動而被重新編譯多次。

缺點:

  • 前置宣告隱藏了依賴關係,標頭檔案改動時,使用者的程式碼會跳過必要的重新編譯過程。
  • 前置宣告可能會被庫的後續更改所破壞。前置宣告函式或模板有時會妨礙標頭檔案開發者變動其 API. 例如擴大形參型別,加個自帶預設引數的模板形參等等。
  • 前置宣告來自名稱空間 std:: 的 symbol 時,其行為未定義。
  • 很難判斷什麼時候該用前置宣告,什麼時候該用 #include 。極端情況下,用前置宣告代替 includes 甚至都會暗暗地改變程式碼的含義:

如果 #include 被 B 和 D 的前置宣告替代, test() 就會呼叫 f(void*) . * 前置宣告瞭不少來自標頭檔案的 symbol 時,就會比單單一行的 include 冗長。 * 僅僅為了能前置宣告而重構程式碼(比如用指標成員代替物件成員)會使程式碼變得更慢更復雜.

結論:

  • 儘量避免前置宣告那些定義在其他專案中的實體.
  • 函式:總是使用 #include.
  • 類别範本:優先使用 #include.

至於什麼時候包含標頭檔案,參見 name-and-order-of-includes。

1.4. 行內函數

只有當函式只有 10 行甚至更少時才將其定義為行內函數.

定義:

當函式被宣告為行內函數之後, 編譯器會將其內聯展開, 而不是按通常的函式呼叫機制進行呼叫.

優點:

只要內聯的函式體較小, 內聯該函式可以令目的碼更加高效. 對於存取函式以及其它函式體比較短, 效能關鍵的函式, 鼓勵使用內聯.

缺點:

濫用內聯將導致程式變得更慢. 內聯可能使目的碼量或增或減, 這取決於行內函數的大小. 內聯非常短小的存取函式通常會減少程式碼大小, 但內聯一個相當大的函式將戲劇性的增加程式碼大小. 現代處理器由於更好的利用了指令快取, 小巧的程式碼往往執行更快。

結論:

一個較為合理的經驗準則是, 不要內聯超過 10 行的函式. 謹慎對待解構函式, 解構函式往往比其表面看起來要更長, 因為有隱含的成員和基類解構函式被呼叫!

另一個實用的經驗準則: 內聯那些包含迴圈或 switch 語句的函式常常是得不償失 (除非在大多數情況下, 這些迴圈或 switch 語句從不被執行).

有些函式即使宣告為內聯的也不一定會被編譯器內聯, 這點很重要; 比如虛擬函式和遞迴函式就不會被正常內聯. 通常, 遞迴函式不應該宣告成行內函數.(YuleFox 注: 遞迴呼叫堆疊的展開並不像迴圈那麼簡單, 比如遞迴層數在編譯時可能是未知的, 大多數編譯器都不支援內聯遞迴函式). 虛擬函式內聯的主要原因則是想把它的函式體放在類定義內, 為了圖個方便, 抑或是當作文件描述其行為, 比如精短的存取函式.

1.5. #include 的路徑及順序

使用標準的標頭檔案包含順序可增強可讀性, 避免隱藏依賴: 相關標頭檔案, 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 檔案

這種優先的順序排序保證當 dir2/foo2.h 遺漏某些必要的庫時, dir/foo.cc 或 dir/foo_test.cc 的構建會立刻中止。因此這一條規則保證維護這些檔案的人們首先看到構建中止的訊息而不是維護其他包的人們。

dir/foo.cc 和 dir2/foo2.h 通常位於同一目錄下 (如 base/basictypes_unittest.cc 和 base/basictypes.h), 但也可以放在不同目錄下.

按字母順序對標頭檔案包含進行二次排序是不錯的主意。注意較老的程式碼可不符合這條規則,要在方便的時候改正它們。

您所依賴的 symbols 被哪些標頭檔案所定義,您就應該包含(include)哪些標頭檔案,forward-declaration 情況除外。比如您要用到 bar.h 中的某個 symbol, 哪怕您所包含的 foo.h 已經包含了 bar.h, 也照樣得包含 bar.h, 除非 foo.h 有明確說明它會自動向您提供 bar.h 中的 symbol. 不過,凡是 cc 檔案所對應的「相關標頭檔案」已經包含的,就不用再重複包含進其 cc 檔案裡面了,就像 foo.cc 只包含 foo.h 就夠了,不用再管後者所包含的其它內容。

舉例來說, google-awesome-project/src/foo/internal/fooserver.cc 的包含次序如下:

#include "foo/public/fooserver.h" // 優先位置

#include <sys/types.h>
#include <unistd.h>

#include <hash_map>
#include <vector>

#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"

例外:

有時,平臺特定(system-specific)程式碼需要條件編譯(conditional includes),這些程式碼可以放到其它 includes 之後。當然,您的平臺特定程式碼也要夠簡練且獨立,比如:

#include "foo/public/fooserver.h"

#include "base/port.h"  // For LANG_CXX11.

#ifdef LANG_CXX11
#include <initializer_list>
#endif  // LANG_CXX11

譯者 (YuleFox) 筆記

  1. 避免多重包含是學程式設計時最基本的要求;
  2. 前置宣告是為了降低編譯依賴,防止修改一個標頭檔案引發多米諾效應;
  3. 行內函數的合理使用可提高程式碼執行效率;
  4. -inl.h 可提高程式碼可讀性 (一般用不到吧: D);
  5. 標準化函式引數順序可以提高可讀性和易維護性 (對函式引數的堆疊空間有輕微影響, 我以前大多是相同型別放在一起);
  6. 包含檔案的名稱使用 . 和 .. 雖然方便卻易混亂, 使用比較完整的專案路徑看上去很清晰, 很條理, 包含檔案的次序除了美觀之外, 最重要的是可以減少隱藏依賴, 使每個標頭檔案在 “最需要編譯” (對應原始檔處 : D) 的地方編譯, 有人提出庫檔案放在最後, 這樣出錯先是專案內的檔案, 標頭檔案都放在對應原始檔的最前面, 這一點足以保證內部錯誤的及時發現了.

譯者(acgtyrant)筆記

  1. 原來還真有專案用 #includes 來插入文字,且其副檔名 .inc 看上去也很科學。
  2. Google 已經不再提倡 -inl.h 用法。
  3. 注意,前置宣告的類是不完全型別(incomplete type),我們只能定義指向該型別的指標或引用,或者宣告(但不能定義)以不完全型別作為引數或者返回型別的函式。畢竟編譯器不知道不完全型別的定義,我們不能建立其類的任何物件,也不能宣告成類內部的資料成員。
  4. 類內部的函式一般會自動內聯。所以某函式一旦不需要內聯,其定義就不要再放在標頭檔案裡,而是放到對應的 .cc 檔案裡。這樣可以保持標頭檔案的類相當精煉,也很好地貫徹了宣告與定義分離的原則。
  5. 在 #include 中插入空行以分割相關標頭檔案, C 庫, C++ 庫, 其他庫的 .h 和本專案內的 .h 是個好習慣。

本系列文章

相關文章