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