1. 程式人生 > >一、標頭檔案

一、標頭檔案

通常每一個.cc檔案(C++的原始檔)都有一個對應的.h檔案(標頭檔案),也有一些例外,入單元測試程式碼和只包含main()的.cc檔案。

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

下面的規則將引導你規避使用標頭檔案時的各種麻煩。

1.#define 的保護

所有標頭檔案都應該使用#define防止標頭檔案被多重包含(multiple inclusion),命名格式當是:<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_

2.標頭檔案依賴

使用前置宣告(forward declarations)儘量減少.h檔案中#include的數量。

當一個頭檔案被包含的同時會引入了一項新的依賴(dependency),只要該標頭檔案被修改,程式碼就要重新編譯。如果你的標頭檔案包含了其他標頭檔案,這些標頭檔案的的任何改變也將導致那些包含了你的標頭檔案重新編譯。因此,我們寧可儘量減少包含標頭檔案,尤其那些包含了其他標頭檔案的。

使用前置宣告可以顯著減少需要包含的標頭檔案的數量。舉例說明:標頭檔案中用到類File,但不需要訪問File的宣告,則標頭檔案只需前置宣告class File;無需#include “file/base/file.h”。

在標頭檔案如何做到使用類Foo而無需訪問類的定義呢?

  1. 將資料成員型別宣告為Foo*或Foo&;

  2. 引數、返回型別為Foo的函式只宣告(但不定義實現);

  3. 靜態資料成員的型別可以被宣告為Foo,因為靜態資料成員的定義在類定義之外。

另一方面,如果你的類是Foo的子類,或者含有型別Foo的非靜態資料成員,則必須為之包含標頭檔案。

有時,使用指標成員(pointer members,如果是scope_ptr更好)替代物件成員(object members)的確更有意義。然而,這樣的做法會降低程式碼可讀性及執行效率。如果僅僅為了少包含標頭檔案,還是不要這樣替代的好。

當然,.cc檔案無論如何都需要所使用類的定義部分,自然也就會包含若干標頭檔案。

3.行內函數

只有當函式只有10行甚至更少才會將其定義為行內函數(inline function)。

定義(Definition):當函式被宣告為行內函數之後,編譯器可能會將其內聯展開,無需按通常的函式呼叫機制呼叫行內函數。

優點:當函式體積比小的時候,內聯該函式可以令目的碼更加有效。對於存取函式(accessor、mutator)以及其他一些比較短的關鍵執行函式。

缺點:濫用內聯將導致程式變慢,內聯有可能使目的碼或增或減,這取決於被行內函數的大小。內聯較短小的存取函式通常會減少程式碼量,但內聯一個很大的函式(如果編譯器允許的話)將戲劇性增加程式碼量。在現代處理器上,由於更好的利用指令快取(instruction cache),小巧的程式碼往往執行的更快。

結論:一個比較得當的處理規則是,不要內聯超過10行的函式。對於解構函式應該慎重對待。解構函式往往比其表面看起來要長,因為有一些隱式的成員和基類解構函式(如果有的話)被呼叫。

另一有用的處理規則:內含那些包含迴圈或switch語句的函式是得不償失的,除非在大多數情況下,這些迴圈或switch語句從不執行。

重要的是,虛擬函式和遞迴函式即使被宣告為內聯也不一定是行內函數。通常,遞迴函式不應該被宣告為內聯(遞迴呼叫堆疊的展開並不像迴圈那麼簡單,比如遞迴層數在編譯器可能是未知的,大多數編譯器都不支援內聯遞迴函式)。解構函式內聯的主要原因是其定義在類的定義中,為了方便抑或對其行為給出文件。

4. -inl.h檔案

複雜的行內函數的定義,應放在後綴名為-inl.h的標頭檔案中。

在標頭檔案中給出行內函數的定義,可令編譯器將其在呼叫處內聯展開。然而,實現程式碼應該完全放在.cc檔案中,我們不希望.h檔案中出現太多的實現程式碼,除非這樣做有可讀性和效率上的明顯優勢。

如果行內函數的定義比較小、邏輯比較簡單,其實現程式碼可以放在.h檔案中。例如,存取函式的實現理所當然都放在類的定義中。出於實現和呼叫的方便,較複雜的行內函數也可以放到.h檔案中,如果你覺得這樣會使標頭檔案顯得笨重,還可以將其分離到單獨的-inl.h中。這樣即把實現和定義分離開來,當需要包含實現所在的-inl.h即可。

-inl.h檔案還可以用於函式模板的定義,從而使得模板定義可讀性增強。

要提醒一點的是,-inl.h和其他標頭檔案一樣,也需要#define保護。

5. 函式引數順序(Function Parameter Ordering)

定義函式時,引數順序為:輸入引數在前,輸出引數在後。

C/C++函式引數氛圍輸入引數和輸出引數兩種,有時輸入引數也會是輸出(值被修改時)。輸入引數一般傳值或常量引用(const references),輸出引數或輸入/輸出引數為非常量指標(non-const pointer)。對引數排序時,將所有的輸入引數置於輸出引數之前。不要僅僅因為是新添的引數,就將其置於最後,而是應該置於輸出引數之前。

這一點並不必須遵循的規則,輸入/輸出兩用引數(通常是類/結構體變數)混在其中,會使得規則難以遵循。

6. 包含檔案的名稱和次序

將包含次序標準化可增強可讀性、避免隱藏依賴(hidden dependencies,主要是指包含的檔案中編譯時),次序如下:C庫、C++庫、其他庫.h、專案內的.h。

專案內標頭檔案應該按照專案原始碼目錄樹結果排列,並避免使用UNIX檔案路徑.(當前目錄)和..(父目錄)。例如,google-awesome-project/src/base/logging.h應該像這樣被包含:

#include "base/logging.h"

dir/foo.cc主要作用是執行或測試dir2/foo2.h的功能, foo.cc中包含標頭檔案的次序如下:

dir2/foo.h (優先位置,詳細如下)

C系統檔案

C++系統檔案

其他庫標頭檔案

本專案內標頭檔案

這種排序方式可有效減少隱藏依賴,我們希望每一個頭檔案獨立編譯。最簡單的實現方式是將其作為第一個.h檔案包含在對應的.cc中。

dir/foo.cc和dir2/foo2.h通常位於專案目錄下(像base/basictypes_unittest.cc和base/basictypes.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"