類是 C++ 中代碼的基本單元. 顯然, 它們被廣泛使用. 本節(jié)列舉了在寫一個類時的主要注意事項.
Tip
構(gòu)造函數(shù)中只進行那些沒什么意義的 (trivial, YuleFox 注: 簡單初始化對于程序執(zhí)行沒有實際的邏輯意義, 因為成員變量 “有意義” 的值大多不在構(gòu)造函數(shù)中確定) 初始化, 可能的話, 使用
Init()
方法集中初始化有意義的 (non-trivial) 數(shù)據(jù).
定義:在構(gòu)造函數(shù)體中進行初始化操作.優(yōu)點:排版方便, 無需擔心類是否已經(jīng)初始化.缺點:
在構(gòu)造函數(shù)中執(zhí)行操作引起的問題有:
- 構(gòu)造函數(shù)中很難上報錯誤, 不能使用異常.
- 操作失敗會造成對象初始化失敗,進入不確定狀態(tài).
- 如果在構(gòu)造函數(shù)內(nèi)調(diào)用了自身的虛函數(shù), 這類調(diào)用是不會重定向到子類的虛函數(shù)實現(xiàn). 即使當前沒有子類化實現(xiàn), 將來仍是隱患.
- 如果有人創(chuàng)建該類型的全局變量 (雖然違背了上節(jié)提到的規(guī)則), 構(gòu)造函數(shù)將先
main()
一步被調(diào)用, 有可能破壞構(gòu)造函數(shù)中暗含的假設(shè)條件. 例如, gflags [http://code.google.com/p/google-gflags/] 尚未初始化.
結(jié)論:如果對象需要進行有意義的 (non-trivial) 初始化, 考慮使用明確的 Init()
方法并 (或) 增加一個成員標記用于指示對象是否已經(jīng)初始化成功.
Tip
如果一個類定義了若干成員變量又沒有其它構(gòu)造函數(shù), 必須定義一個默認構(gòu)造函數(shù). 否則編譯器將自動生產(chǎn)一個很糟糕的默認構(gòu)造函數(shù).
定義:new
一個不帶參數(shù)的類對象時, 會調(diào)用這個類的默認構(gòu)造函數(shù). 用 new[]
創(chuàng)建數(shù)組時,默認構(gòu)造函數(shù)則總是被調(diào)用.優(yōu)點:默認將結(jié)構(gòu)體初始化為 “無效” 值, 使調(diào)試更方便.缺點:對代碼編寫者來說, 這是多余的工作.結(jié)論:
如果類中定義了成員變量, 而且沒有提供其它構(gòu)造函數(shù), 你必須定義一個 (不帶參數(shù)的) 默認構(gòu)造函數(shù). 把對象的內(nèi)部狀態(tài)初始化成一致/有效的值無疑是更合理的方式.
這么做的原因是: 如果你沒有提供其它構(gòu)造函數(shù), 又沒有定義默認構(gòu)造函數(shù), 編譯器將為你自動生成一個. 編譯器生成的構(gòu)造函數(shù)并不會對對象進行合理的初始化.
如果你定義的類繼承現(xiàn)有類, 而你又沒有增加新的成員變量, 則不需要為新類定義默認構(gòu)造函數(shù).
Tip
對單個參數(shù)的構(gòu)造函數(shù)使用 C++ 關(guān)鍵字
explicit
.
定義:通常, 如果構(gòu)造函數(shù)只有一個參數(shù), 可看成是一種隱式轉(zhuǎn)換. 打個比方, 如果你定義了 Foo::Foo(string name)
, 接著把一個字符串傳給一個以 Foo
對象為參數(shù)的函數(shù), 構(gòu)造函數(shù) Foo::Foo(string name)
將被調(diào)用, 并將該字符串轉(zhuǎn)換為一個 Foo
的臨時對象傳給調(diào)用函數(shù). 看上去很方便, 但如果你并不希望如此通過轉(zhuǎn)換生成一個新對象的話, 麻煩也隨之而來. 為避免構(gòu)造函數(shù)被調(diào)用造成隱式轉(zhuǎn)換, 可以將其聲明為 explicit
.優(yōu)點:避免不合時宜的變換.缺點:無結(jié)論:
所有單參數(shù)構(gòu)造函數(shù)都必須是顯式的. 在類定義中, 將關(guān)鍵字 explicit
加到單參數(shù)構(gòu)造函數(shù)前: explicit Foo(string name);
例外: 在極少數(shù)情況下, 拷貝構(gòu)造函數(shù)可以不聲明成 explicit
. 作為其它類的透明包裝器的類也是特例之一. 類似的例外情況應在注釋中明確說明.
Tip
僅在代碼中需要拷貝一個類對象的時候使用拷貝構(gòu)造函數(shù); 大部分情況下都不需要, 此時應使用
DISALLOW_COPY_AND_ASSIGN
.
定義:拷貝構(gòu)造函數(shù)在復制一個對象到新建對象時被調(diào)用 (特別是對象傳值時).優(yōu)點:拷貝構(gòu)造函數(shù)使得拷貝對象更加容易. STL 容器要求所有內(nèi)容可拷貝, 可賦值.缺點:C++ 中的隱式對象拷貝是很多性能問題和 bug 的根源. 拷貝構(gòu)造函數(shù)降低了代碼可讀性, 相比傳引用, 跟蹤傳值的對象更加困難, 對象修改的地方變得難以捉摸.結(jié)論:
大部分類并不需要可拷貝, 也不需要一個拷貝構(gòu)造函數(shù)或重載賦值運算符. 不幸的是, 如果你不主動聲明它們, 編譯器會為你自動生成, 而且是 public
的.
可以考慮在類的 private:
中添加拷貝構(gòu)造函數(shù)和賦值操作的空實現(xiàn), 只有聲明, 沒有定義. 由于這些空函數(shù)聲明為 private
, 當其他代碼試圖使用它們的時候, 編譯器將報錯. 方便起見, 我們可以使用 DISALLOW_COPY_AND_ASSIGN
宏:
// 禁止使用拷貝構(gòu)造函數(shù)和 operator= 賦值操作的宏
// 應該類的 private: 中使用
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
void operator=(const TypeName&)
在 class foo:
中:
class Foo {
public:
Foo(int f);
~Foo();
private:
DISALLOW_COPY_AND_ASSIGN(Foo);
};
如上所述, 絕大多數(shù)情況下都應使用 DISALLOW_COPY_AND_ASSIGN
宏. 如果類確實需要可拷貝, 應在該類的頭文件中說明原由, 并合理的定義拷貝構(gòu)造函數(shù)和賦值操作. 注意在 operator=
中檢測自我賦值的情況 (yospaly 注: 即 operator=
接收的參數(shù)是該對象本身).
為了能作為 STL 容器的值, 你可能有使類可拷貝的沖動. 在大多數(shù)類似的情況下, 真正該做的是把對象的 指針 放到 STL 容器中. 可以考慮使用 std::tr1::shared_ptr
.
Tip
僅當只有數(shù)據(jù)時使用
struct
, 其它一概使用class
.
在 C++ 中 struct
和 class
關(guān)鍵字幾乎含義一樣. 我們?yōu)檫@兩個關(guān)鍵字添加我們自己的語義理解, 以便為定義的數(shù)據(jù)類型選擇合適的關(guān)鍵字.
struct
用來定義包含數(shù)據(jù)的被動式對象, 也可以包含相關(guān)的常量, 但除了存取數(shù)據(jù)成員之外, 沒有別的函數(shù)功能. 并且存取功能是通過直接訪問位域 (field), 而非函數(shù)調(diào)用. 除了構(gòu)造函數(shù), 析構(gòu)函數(shù), Initialize()
, Reset()
, Validate()
外, 不能提供其它功能的函數(shù).
如果需要更多的函數(shù)功能, class
更適合. 如果拿不準, 就用 class
.
為了和 STL 保持一致, 對于仿函數(shù) (functors) 和特性 (traits) 可以不用 class
而是使用 struct
.
注意: 類和結(jié)構(gòu)體的成員變量使用 不同的命名規(guī)則.
Tip
使用組合 (composition, YuleFox 注: 這一點也是 GoF 在 <> 里反復強調(diào)的) 常常比使用繼承更合理. 如果使用繼承的話, 定義為
public
繼承.
定義:當子類繼承基類時, 子類包含了父基類所有數(shù)據(jù)及操作的定義. C++ 實踐中, 繼承主要用于兩種場合: 實現(xiàn)繼承 (implementation inheritance), 子類繼承父類的實現(xiàn)代碼; 接口繼承 (interface inheritance), 子類僅繼承父類的方法名稱.優(yōu)點:實現(xiàn)繼承通過原封不動的復用基類代碼減少了代碼量. 由于繼承是在編譯時聲明, 程序員和編譯器都可以理解相應操作并發(fā)現(xiàn)錯誤. 從編程角度而言, 接口繼承是用來強制類輸出特定的 API. 在類沒有實現(xiàn) API 中某個必須的方法時, 編譯器同樣會發(fā)現(xiàn)并報告錯誤.缺點:對于實現(xiàn)繼承, 由于子類的實現(xiàn)代碼散布在父類和子類間之間, 要理解其實現(xiàn)變得更加困難. 子類不能重寫父類的非虛函數(shù), 當然也就不能修改其實現(xiàn). 基類也可能定義了一些數(shù)據(jù)成員, 還要區(qū)分基類的實際布局.結(jié)論:
所有繼承必須是 public
的. 如果你想使用私有繼承, 你應該替換成把基類的實例作為成員對象的方式.
不要過度使用實現(xiàn)繼承. 組合常常更合適一些. 盡量做到只在 “是一個” (“is-a”, YuleFox 注: 其他 “has-a” 情況下請使用組合) 的情況下使用繼承: 如果 Bar
的確 “是一種” Foo, Bar
才能繼承 Foo
.
必要的話, 析構(gòu)函數(shù)聲明為 virtual
. 如果你的類有虛函數(shù), 則析構(gòu)函數(shù)也應該為虛函數(shù). 注意 數(shù)據(jù)成員在任何情況下都必須是私有的.
當重載一個虛函數(shù), 在衍生類中把它明確的聲明為 virtual
. 理論依據(jù): 如果省略 virtual
關(guān)鍵字, 代碼閱讀者不得不檢查所有父類, 以判斷該函數(shù)是否是虛函數(shù).
Tip
真正需要用到多重實現(xiàn)繼承的情況少之又少. 只在以下情況我們才允許多重繼承: 最多只有一個基類是非抽象類; 其它基類都是以
Interface
為后綴的純接口類.
定義:多重繼承允許子類擁有多個基類. 要將作為 純接口 的基類和具有 實現(xiàn) 的基類區(qū)別開來.優(yōu)點:相比單繼承 (見 繼承), 多重實現(xiàn)繼承可以復用更多的代碼.缺點:真正需要用到多重 實現(xiàn) 繼承的情況少之又少. 多重實現(xiàn)繼承看上去是不錯的解決方案, 但你通常也可以找到一個更明確, 更清晰的不同解決方案.結(jié)論:只有當所有父類除第一個外都是 純接口類 時, 才允許使用多重繼承. 為確保它們是純接口, 這些類必須以 Interface
為后綴.
Note
關(guān)于該規(guī)則, Windows 下有個 特例.
Tip
接口是指滿足特定條件的類, 這些類以
Interface
為后綴 (不強制).
定義:
當一個類滿足以下要求時, 稱之為純接口:
- 只有純虛函數(shù) (“
=0
”) 和靜態(tài)函數(shù) (除了下文提到的析構(gòu)函數(shù)).- 沒有非靜態(tài)數(shù)據(jù)成員.
- 沒有定義任何構(gòu)造函數(shù). 如果有, 也不能帶有參數(shù), 并且必須為
protected
.- 如果它是一個子類, 也只能從滿足上述條件并以
Interface
為后綴的類繼承.
接口類不能被直接實例化, 因為它聲明了純虛函數(shù). 為確保接口類的所有實現(xiàn)可被正確銷毀, 必須為之聲明虛析構(gòu)函數(shù) (作為上述第 1 條規(guī)則的特例, 析構(gòu)函數(shù)不能是純虛函數(shù)). 具體細節(jié)可參考 Stroustrup 的 The C++ Programming Language, 3rd edition 第 12.4 節(jié).
優(yōu)點:以 Interface
為后綴可以提醒其他人不要為該接口類增加函數(shù)實現(xiàn)或非靜態(tài)數(shù)據(jù)成員. 這一點對于 多重繼承 尤其重要. 另外, 對于 Java 程序員來說, 接口的概念已是深入人心.缺點:Interface
后綴增加了類名長度, 為閱讀和理解帶來不便. 同時,接口特性作為實現(xiàn)細節(jié)不應暴露給用戶.結(jié)論:只有在滿足上述需要時, 類才以 Interface
結(jié)尾, 但反過來, 滿足上述需要的類未必一定以 Interface
結(jié)尾.
Tip
除少數(shù)特定環(huán)境外,不要重載運算符.
定義:一個類可以定義諸如 +
和 /
等運算符, 使其可以像內(nèi)建類型一樣直接操作.優(yōu)點:使代碼看上去更加直觀, 類表現(xiàn)的和內(nèi)建類型 (如 int
) 行為一致. 重載運算符使 Equals()
, Add()
等函數(shù)名黯然失色. 為了使一些模板函數(shù)正確工作, 你可能必須定義操作符.缺點:
雖然操作符重載令代碼更加直觀, 但也有一些不足:
Equals()
顯然比對應的 ==
調(diào)用點要容易的多.Foo + 4
做的是一件事, 而 &Foo + 4
可能做的是完全不同的另一件事. 對于二者, 編譯器都不會報錯, 使其很難調(diào)試;重載還有令你吃驚的副作用. 比如, 重載了 operator&
的類不能被前置聲明.
結(jié)論:
一般不要重載運算符. 尤其是賦值操作 (operator=
) 比較詭異, 應避免重載. 如果需要的話, 可以定義類似 Equals()
, CopyFrom()
等函數(shù).
然而, 極少數(shù)情況下可能需要重載運算符以便與模板或 “標準” C++ 類互操作 (如 operator<<(ostream&, const T&)
). 只有被證明是完全合理的才能重載, 但你還是要盡可能避免這樣做. 尤其是不要僅僅為了在 STL 容器中用作鍵值就重載 operator==
或 operator<
; 相反, 你應該在聲明容器的時候, 創(chuàng)建相等判斷和大小比較的仿函數(shù)類型.
有些 STL 算法確實需要重載 operator==
時, 你可以這么做, 記得別忘了在文檔中說明原因.
參考 拷貝構(gòu)造函數(shù) 和 函數(shù)重載.
Tip
將 所有 數(shù)據(jù)成員聲明為
private
, 并根據(jù)需要提供相應的存取函數(shù). 例如, 某個名為foo_
的變量, 其取值函數(shù)是foo()
. 還可能需要一個賦值函數(shù)set_foo()
.
一般在頭文件中把存取函數(shù)定義成內(nèi)聯(lián)函數(shù).
Tip
在類中使用特定的聲明順序:
public:
在private:
之前, 成員函數(shù)在數(shù)據(jù)成員 (變量) 前;
類的訪問控制區(qū)段的聲明順序依次為: public:
, protected:
, private:
. 如果某區(qū)段沒內(nèi)容, 可以不聲明.
每個區(qū)段內(nèi)的聲明通常按以下順序:
typedefs
和枚舉- 常量
- 構(gòu)造函數(shù)
- 析構(gòu)函數(shù)
- 成員函數(shù), 含靜態(tài)成員函數(shù)
- 數(shù)據(jù)成員, 含靜態(tài)數(shù)據(jù)成員
宏 DISALLOW_COPY_AND_ASSIGN
的調(diào)用放在 private:
區(qū)段的末尾. 它通常是類的最后部分. 參考 拷貝構(gòu)造函數(shù).
.cc
文件中函數(shù)的定義應盡可能和聲明順序一致.
不要在類定義中內(nèi)聯(lián)大型函數(shù). 通常, 只有那些沒有特別意義或性能要求高, 并且是比較短小的函數(shù)才能被定義為內(nèi)聯(lián)函數(shù). 更多細節(jié)參考 內(nèi)聯(lián)函數(shù).
Tip
傾向編寫簡短, 凝練的函數(shù).
我們承認長函數(shù)有時是合理的, 因此并不硬性限制函數(shù)的長度. 如果函數(shù)超過 40 行, 可以思索一下能不能在不影響程序結(jié)構(gòu)的前提下對其進行分割.
即使一個長函數(shù)現(xiàn)在工作的非常好, 一旦有人對其修改, 有可能出現(xiàn)新的問題. 甚至導致難以發(fā)現(xiàn)的 bug. 使函數(shù)盡量簡短, 便于他人閱讀和修改代碼.
在處理代碼時, 你可能會發(fā)現(xiàn)復雜的長函數(shù). 不要害怕修改現(xiàn)有代碼: 如果證實這些代碼使用 / 調(diào)試困難, 或者你需要使用其中的一小段代碼, 考慮將其分割為更加簡短并易于管理的若干函數(shù).
explicit
;private
且無需實現(xiàn);struct
;virtual
關(guān)鍵字, 雖然編譯器允許不這樣做;Interface
為后綴, 除提供帶實現(xiàn)的虛析構(gòu)函數(shù), 靜態(tài)成員函數(shù)外, 其他均為純虛函數(shù), 不定義非靜態(tài)數(shù)據(jù)成員, 不提供構(gòu)造函數(shù), 提供的話,聲明為 protected
;public
-> protected
-> private
;
更多建議: