3. 類(lèi)

2018-02-24 15:11 更新

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

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

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)初始化成功.

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

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ù).

3.3. 顯式構(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ō)明.

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

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.

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

Tip

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

在 C++ 中 structclass 關(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ī)則.

3.6. 繼承

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ù).

3.7. 多重繼承

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è) 特例.

3.8. 接口

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é)尾.

3.9. 運(yùn)算符重載

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):
雖然操作符重載令代碼更加直觀, 但也有一些不足:

  • 混淆視聽(tīng), 讓你誤以為一些耗時(shí)的操作和操作內(nèi)建類(lèi)型一樣輕巧.
  • 更難定位重載運(yùn)算符的調(diào)用點(diǎn), 查找 Equals() 顯然比對(duì)應(yīng)的 == 調(diào)用點(diǎn)要容易的多.
  • 有的運(yùn)算符可以對(duì)指針進(jìn)行操作, 容易導(dǎo)致 bug. 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ù)重載.

3.10. 存取控制

Tip

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

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

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

3.11. 聲明順序

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ù).

3.12. 編寫(xiě)簡(jiǎ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ù).

譯者 (YuleFox) 筆記

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)