3. 類

2018-02-24 15:11 更新

類是 C++ 中代碼的基本單元. 顯然, 它們被廣泛使用. 本節(jié)列舉了在寫一個類時的主要注意事項.

3.1. 構(gòu)造函數(shù)的職責(zé)

Tip

構(gòu)造函數(shù)中只進(jìn)行那些沒什么意義的 (trivial, YuleFox 注: 簡單初始化對于程序執(zhí)行沒有實際的邏輯意義, 因為成員變量 “有意義” 的值大多不在構(gòu)造函數(shù)中確定) 初始化, 可能的話, 使用 Init() 方法集中初始化有意義的 (non-trivial) 數(shù)據(jù).

定義:在構(gòu)造函數(shù)體中進(jìn)行初始化操作.優(yōu)點:排版方便, 無需擔(dān)心類是否已經(jīng)初始化.缺點:
在構(gòu)造函數(shù)中執(zhí)行操作引起的問題有:

  • 構(gòu)造函數(shù)中很難上報錯誤, 不能使用異常.
  • 操作失敗會造成對象初始化失敗,進(jìn)入不確定狀態(tài).
  • 如果在構(gòu)造函數(shù)內(nèi)調(diào)用了自身的虛函數(shù), 這類調(diào)用是不會重定向到子類的虛函數(shù)實現(xiàn). 即使當(dāng)前沒有子類化實現(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é)論:如果對象需要進(jìn)行有意義的 (non-trivial) 初始化, 考慮使用明確的 Init() 方法并 (或) 增加一個成員標(biāo)記用于指示對象是否已經(jīng)初始化成功.

3.2. 默認(rèn)構(gòu)造函數(shù)

Tip

如果一個類定義了若干成員變量又沒有其它構(gòu)造函數(shù), 必須定義一個默認(rèn)構(gòu)造函數(shù). 否則編譯器將自動生產(chǎn)一個很糟糕的默認(rèn)構(gòu)造函數(shù).

定義:new 一個不帶參數(shù)的類對象時, 會調(diào)用這個類的默認(rèn)構(gòu)造函數(shù). 用 new[] 創(chuàng)建數(shù)組時,默認(rèn)構(gòu)造函數(shù)則總是被調(diào)用.優(yōu)點:默認(rèn)將結(jié)構(gòu)體初始化為 “無效” 值, 使調(diào)試更方便.缺點:對代碼編寫者來說, 這是多余的工作.結(jié)論:
如果類中定義了成員變量, 而且沒有提供其它構(gòu)造函數(shù), 你必須定義一個 (不帶參數(shù)的) 默認(rèn)構(gòu)造函數(shù). 把對象的內(nèi)部狀態(tài)初始化成一致/有效的值無疑是更合理的方式.

這么做的原因是: 如果你沒有提供其它構(gòu)造函數(shù), 又沒有定義默認(rèn)構(gòu)造函數(shù), 編譯器將為你自動生成一個. 編譯器生成的構(gòu)造函數(shù)并不會對對象進(jìn)行合理的初始化.

如果你定義的類繼承現(xiàn)有類, 而你又沒有增加新的成員變量, 則不需要為新類定義默認(rèn)構(gòu)造函數(shù).

3.3. 顯式構(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. 作為其它類的透明包裝器的類也是特例之一. 類似的例外情況應(yīng)在注釋中明確說明.

3.4. 拷貝構(gòu)造函數(shù)

Tip

僅在代碼中需要拷貝一個類對象的時候使用拷貝構(gòu)造函數(shù); 大部分情況下都不需要, 此時應(yīng)使用 DISALLOW_COPY_AND_ASSIGN.

定義:拷貝構(gòu)造函數(shù)在復(fù)制一個對象到新建對象時被調(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, 當(dāng)其他代碼試圖使用它們的時候, 編譯器將報錯. 方便起見, 我們可以使用 DISALLOW_COPY_AND_ASSIGN 宏:

// 禁止使用拷貝構(gòu)造函數(shù)和 operator= 賦值操作的宏
// 應(yīng)該類的 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 宏. 如果類確實需要可拷貝, 應(yīng)在該類的頭文件中說明原由, 并合理的定義拷貝構(gòu)造函數(shù)和賦值操作. 注意在 operator= 中檢測自我賦值的情況 (yospaly 注: 即 operator= 接收的參數(shù)是該對象本身).

為了能作為 STL 容器的值, 你可能有使類可拷貝的沖動. 在大多數(shù)類似的情況下, 真正該做的是把對象的 指針 放到 STL 容器中. 可以考慮使用 std::tr1::shared_ptr.

3.5. 結(jié)構(gòu)體 VS. 類

Tip

僅當(dāng)只有數(shù)據(jù)時使用 struct, 其它一概使用 class.

在 C++ 中 structclass 關(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 更適合. 如果拿不準(zhǔn), 就用 class.

為了和 STL 保持一致, 對于仿函數(shù) (functors) 和特性 (traits) 可以不用 class 而是使用 struct.

注意: 類和結(jié)構(gòu)體的成員變量使用 不同的命名規(guī)則.

3.6. 繼承

Tip

使用組合 (composition, YuleFox 注: 這一點也是 GoF 在 <> 里反復(fù)強調(diào)的) 常常比使用繼承更合理. 如果使用繼承的話, 定義為 public 繼承.

定義:當(dāng)子類繼承基類時, 子類包含了父基類所有數(shù)據(jù)及操作的定義. C++ 實踐中, 繼承主要用于兩種場合: 實現(xiàn)繼承 (implementation inheritance), 子類繼承父類的實現(xiàn)代碼; 接口繼承 (interface inheritance), 子類僅繼承父類的方法名稱.優(yōu)點:實現(xiàn)繼承通過原封不動的復(fù)用基類代碼減少了代碼量. 由于繼承是在編譯時聲明, 程序員和編譯器都可以理解相應(yīng)操作并發(fā)現(xiàn)錯誤. 從編程角度而言, 接口繼承是用來強制類輸出特定的 API. 在類沒有實現(xiàn) API 中某個必須的方法時, 編譯器同樣會發(fā)現(xiàn)并報告錯誤.缺點:對于實現(xiàn)繼承, 由于子類的實現(xiàn)代碼散布在父類和子類間之間, 要理解其實現(xiàn)變得更加困難. 子類不能重寫父類的非虛函數(shù), 當(dāng)然也就不能修改其實現(xiàn). 基類也可能定義了一些數(shù)據(jù)成員, 還要區(qū)分基類的實際布局.結(jié)論:
所有繼承必須是 public 的. 如果你想使用私有繼承, 你應(yīng)該替換成把基類的實例作為成員對象的方式.

不要過度使用實現(xiàn)繼承. 組合常常更合適一些. 盡量做到只在 “是一個” (“is-a”, YuleFox 注: 其他 “has-a” 情況下請使用組合) 的情況下使用繼承: 如果 Bar 的確 “是一種” Foo, Bar 才能繼承 Foo.

必要的話, 析構(gòu)函數(shù)聲明為 virtual. 如果你的類有虛函數(shù), 則析構(gòu)函數(shù)也應(yīng)該為虛函數(shù). 注意 數(shù)據(jù)成員在任何情況下都必須是私有的.

當(dāng)重載一個虛函數(shù), 在衍生類中把它明確的聲明為 virtual. 理論依據(jù): 如果省略 virtual 關(guān)鍵字, 代碼閱讀者不得不檢查所有父類, 以判斷該函數(shù)是否是虛函數(shù).

3.7. 多重繼承

Tip

真正需要用到多重實現(xiàn)繼承的情況少之又少. 只在以下情況我們才允許多重繼承: 最多只有一個基類是非抽象類; 其它基類都是以 Interface 為后綴的純接口類.

定義:多重繼承允許子類擁有多個基類. 要將作為 純接口 的基類和具有 實現(xiàn) 的基類區(qū)別開來.優(yōu)點:相比單繼承 (見 繼承), 多重實現(xiàn)繼承可以復(fù)用更多的代碼.缺點:真正需要用到多重 實現(xiàn) 繼承的情況少之又少. 多重實現(xiàn)繼承看上去是不錯的解決方案, 但你通常也可以找到一個更明確, 更清晰的不同解決方案.結(jié)論:只有當(dāng)所有父類除第一個外都是 純接口類 時, 才允許使用多重繼承. 為確保它們是純接口, 這些類必須以 Interface 為后綴.

Note

關(guān)于該規(guī)則, Windows 下有個 特例.

3.8. 接口

Tip

接口是指滿足特定條件的類, 這些類以 Interface 為后綴 (不強制).

定義:
當(dāng)一個類滿足以下要求時, 稱之為純接口:

  • 只有純虛函數(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ù)). 具體細(xì)節(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)細(xì)節(jié)不應(yīng)暴露給用戶.結(jié)論:只有在滿足上述需要時, 類才以 Interface 結(jié)尾, 但反過來, 滿足上述需要的類未必一定以 Interface 結(jié)尾.

3.9. 運算符重載

Tip

除少數(shù)特定環(huán)境外,不要重載運算符.

定義:一個類可以定義諸如 +/ 等運算符, 使其可以像內(nèi)建類型一樣直接操作.優(yōu)點:使代碼看上去更加直觀, 類表現(xiàn)的和內(nèi)建類型 (如 int) 行為一致. 重載運算符使 Equals(), Add() 等函數(shù)名黯然失色. 為了使一些模板函數(shù)正確工作, 你可能必須定義操作符.缺點:
雖然操作符重載令代碼更加直觀, 但也有一些不足:

  • 混淆視聽, 讓你誤以為一些耗時的操作和操作內(nèi)建類型一樣輕巧.
  • 更難定位重載運算符的調(diào)用點, 查找 Equals() 顯然比對應(yīng)的 == 調(diào)用點要容易的多.
  • 有的運算符可以對指針進(jìn)行操作, 容易導(dǎo)致 bug. Foo + 4 做的是一件事, 而 &Foo + 4 可能做的是完全不同的另一件事. 對于二者, 編譯器都不會報錯, 使其很難調(diào)試;

重載還有令你吃驚的副作用. 比如, 重載了 operator& 的類不能被前置聲明.

結(jié)論:
一般不要重載運算符. 尤其是賦值操作 (operator=) 比較詭異, 應(yīng)避免重載. 如果需要的話, 可以定義類似 Equals(), CopyFrom() 等函數(shù).

然而, 極少數(shù)情況下可能需要重載運算符以便與模板或 “標(biāo)準(zhǔn)” C++ 類互操作 (如 operator<<(ostream&, const T&)). 只有被證明是完全合理的才能重載, 但你還是要盡可能避免這樣做. 尤其是不要僅僅為了在 STL 容器中用作鍵值就重載 operator==operator<; 相反, 你應(yīng)該在聲明容器的時候, 創(chuàng)建相等判斷和大小比較的仿函數(shù)類型.

有些 STL 算法確實需要重載 operator== 時, 你可以這么做, 記得別忘了在文檔中說明原因.

參考 拷貝構(gòu)造函數(shù)函數(shù)重載.

3.10. 存取控制

Tip

所有 數(shù)據(jù)成員聲明為 private, 并根據(jù)需要提供相應(yīng)的存取函數(shù). 例如, 某個名為 foo_ 的變量, 其取值函數(shù)是 foo(). 還可能需要一個賦值函數(shù) set_foo().

一般在頭文件中把存取函數(shù)定義成內(nèi)聯(lián)函數(shù).

參考 繼承函數(shù)命名

3.11. 聲明順序

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ù)的定義應(yīng)盡可能和聲明順序一致.

不要在類定義中內(nèi)聯(lián)大型函數(shù). 通常, 只有那些沒有特別意義或性能要求高, 并且是比較短小的函數(shù)才能被定義為內(nèi)聯(lián)函數(shù). 更多細(xì)節(jié)參考 內(nèi)聯(lián)函數(shù).

3.12. 編寫簡短函數(shù)

Tip

傾向編寫簡短, 凝練的函數(shù).

我們承認(rèn)長函數(shù)有時是合理的, 因此并不硬性限制函數(shù)的長度. 如果函數(shù)超過 40 行, 可以思索一下能不能在不影響程序結(jié)構(gòu)的前提下對其進(jìn)行分割.

即使一個長函數(shù)現(xiàn)在工作的非常好, 一旦有人對其修改, 有可能出現(xiàn)新的問題. 甚至導(dǎo)致難以發(fā)現(xiàn)的 bug. 使函數(shù)盡量簡短, 便于他人閱讀和修改代碼.

在處理代碼時, 你可能會發(fā)現(xiàn)復(fù)雜的長函數(shù). 不要害怕修改現(xiàn)有代碼: 如果證實這些代碼使用 / 調(diào)試?yán)щy, 或者你需要使用其中的一小段代碼, 考慮將其分割為更加簡短并易于管理的若干函數(shù).

譯者 (YuleFox) 筆記

  1. 不在構(gòu)造函數(shù)中做太多邏輯相關(guān)的初始化;
  2. 編譯器提供的默認(rèn)構(gòu)造函數(shù)不會對變量進(jìn)行初始化, 如果定義了其他構(gòu)造函數(shù), 編譯器不再提供, 需要編碼者自行提供默認(rèn)構(gòu)造函數(shù);
  3. 為避免隱式轉(zhuǎn)換, 需將單參數(shù)構(gòu)造函數(shù)聲明為 explicit;
  4. 為避免拷貝構(gòu)造函數(shù), 賦值操作的濫用和編譯器自動生成, 可將其聲明為 private 且無需實現(xiàn);
  5. 僅在作為數(shù)據(jù)集合時使用 struct;
  6. 組合 > 實現(xiàn)繼承 > 接口繼承 > 私有繼承, 子類重載的虛函數(shù)也要聲明 virtual 關(guān)鍵字, 雖然編譯器允許不這樣做;
  7. 避免使用多重繼承, 使用時, 除一個基類含有實現(xiàn)外, 其他基類均為純接口;
  8. 接口類類名以 Interface 為后綴, 除提供帶實現(xiàn)的虛析構(gòu)函數(shù), 靜態(tài)成員函數(shù)外, 其他均為純虛函數(shù), 不定義非靜態(tài)數(shù)據(jù)成員, 不提供構(gòu)造函數(shù), 提供的話,聲明為 protected;
  9. 為降低復(fù)雜性, 盡量不重載操作符, 模板, 標(biāo)準(zhǔn)類中使用時提供文檔說明;
  10. 存取函數(shù)一般內(nèi)聯(lián)在頭文件中;
  11. 聲明次序: public -> protected -> private;
  12. 函數(shù)體盡量短小, 緊湊, 功能單一;
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號