構造函數的職責
不要在構造函數中進行復雜的初始化 (尤其是那些有可能失敗或者需要調用虛函數的初始化).
在構造函數體中進行初始化操作.
排版方便, 無需擔心類是否已經初始化.
- 構造函數中很難上報錯誤, 不能使用異常.
- 操作失敗會造成對象初始化失敗,進入不確定狀態.
- 如果在構造函數內調用了自身的虛函數, 這類調用是不會重定向到子類的虛函數實現. 即使當前沒有子類化實現, 將來仍是隱患.
- 如果有人創建該類型的全局變量 (雖然違背了上節提到的規則), 構造函數將先 main() 一步被調用, 有可能破壞構造函數中暗含的假設條件. 例如, gflags 尚未初始化.
構造函數不得調用虛函數, 或嘗試報告一個非致命錯誤. 如果對象需要進行有意義的 (non-trivial) 初始化, 考慮使用明確的 Init() 方法或使用工廠模式.
3.2. 初始化
new 一個不帶參數的類對象時, 會調用這個類的默認構造函數. 用 new[] 創建數組時, 默認構造函數則總是被調用. 在類成員裏面進行初始化是指聲明一個成員變量的時候使用一個結構例如 int _count = 17 或者 string _name{"abc"} 來替代 int _count 或者 string _name 這樣的形式.
用戶定義的默認構造函數將在沒有提供初始化操作時將對象初始化. 這樣就保證了對象在被構造之時就處於一個有效且可用的狀態, 同時保證了對象在被創建時就處於一個顯然”不可能”的狀態, 以此幫助調試.
對代碼編寫者來說, 這是多餘的工作.如果一個成員變量在聲明時初始化又在構造函數中初始化, 有可能造成混亂, 因爲構造函數中的值會覆蓋掉聲明中的值.
簡單的初始化用類成員初始化完成, 尤其是當一個成員變量要在多個構造函數裏用相同的方式初始化的時候.
3.3. 顯式構造函數
通常, 如果構造函數只有一個參數, 可看成是一種隱式轉換. 打個比方, 如果你定義了 Foo::Foo(string name), 接着把一個字符串傳給一個以 Foo 對象爲參數的函數, 構造函數 Foo::Foo(string name) 將被調用, 並將該字符串轉換爲一個 Foo 的臨時對象傳給調用函數. 看上去很方便, 但如果你並不希望如此通過轉換生成一個新對象的話, 麻煩也隨之而來. 爲避免構造函數被調用造成隱式轉換, 可以將其聲明爲 explicit.除單參數構造函數外, 這一規則也適用於除第一個參數以外的其他參數都具有默認參數的構造函數, 例如 Foo::Foo(string name, int id = 42).
避免不合時宜的變換.
無
MyType m = {1, 2};MyType MakeMyType() { return {1, 2}; }TakeMyType({1, 2});
3.4. 可拷貝類型和可移動類型
拷貝構造函數是隱式調用的, 也就是說, 這些調用很容易被忽略. 這會讓人迷惑, 尤其是對那些所用的語言約定或強制要求傳引用的程序員來說更是如此. 同時, 這從一定程度上說會鼓勵過度拷貝, 從而導致性能上的問題.
3.5. 委派和繼承構造函數
X::X(const string& name) : name_(name) { ...}X::X() : X("") { }
class Base { public: Base(); Base(int n); Base(const string& s); ...};class Derived : public Base { public: using Base::Base; // Base's constructors are redeclared here.};
如果派生類的構造函數只是調用基類的構造函數而沒有其他行爲時, 這一功能特別有用.
委派和繼承構造函數可以減少冗餘代碼, 提高可讀性. 委派構造函數對 Java 程序員來說並不陌生.
使用輔助函數可以預估出委派構造函數的行爲. 如果派生類和基類相比引入了新的成員變量, 繼承構造函數就會讓人迷惑, 因爲基類並不知道這些新的成員變量的存在.
只在能夠減少冗餘代碼, 提高可讀性的前提下使用委派和繼承構造函數. 如果派生類有新的成員變量, 那麼使用繼承構造函數時要小心. 如果在派生類中對成員變量使用了類內部初始化的話, 繼承構造函數還是適用的.
3.6. 結構體 VS. 類
在 C++ 中 struct 和 class 關鍵字幾乎含義一樣. 我們爲這兩個關鍵字添加我們自己的語義理解, 以便未定義的數據類型選擇合適的關鍵字.struct 用來定義包含數據的被動式對象, 也可以包含相關的常量, 但除了存取數據成員之外, 沒有別的函數功能. 並且存取功能是通過直接訪問位域, 而非函數調用. 除了構造函數, 析構函數, Initialize(), Reset(), Validate() 等類似的函數外, 不能提供其它功能的函數.如果需要更多的函數功能, class 更適合. 如果拿不準, 就用 class.爲了和 STL 保持一致, 對於仿函數和 trait 特性可以不用 class 而是使用 struct.注意: 類和結構體的成員變量使用不同的命名規則.
3.7. 繼承
當子類繼承基類時, 子類包含了父基類所有數據及操作的定義. C++ 實踐中, 繼承主要用於兩種場合: 實現繼承 (implementation inheritance), 子類繼承父類的實現代碼; 接口繼承 (interface inheritance), 子類僅繼承父類的方法名稱.
實現繼承通過原封不動的複用基類代碼減少了代碼量. 由於繼承是在編譯時聲明, 程序員和編譯器都可以理解相應操作並發現錯誤. 從編程角度而言, 接口繼承是用來強制類輸出特定的 API. 在類沒有實現 API 中某個必須的方法時, 編譯器同樣會發現並報告錯誤.
對於實現繼承, 由於子類的實現代碼散佈在父類和子類間之間, 要理解其實現變得更加困難. 子類不能重寫父類的非虛函數, 當然也就不能修改其實現. 基類也可能定義了一些數據成員, 還要區分基類的實際佈局.
所有繼承必須是 public 的. 如果你想使用私有繼承, 你應該替換成把基類的實例作爲成員對象的方式.不要過度使用實現繼承. 組合常常更合適一些. 儘量做到只在 “是一個” (“is-a”, YuleFox 注: 其他 “has-a” 情況下請使用組合) 的情況下使用繼承: 如果 Bar 的確 “是一種” Foo, Bar 才能繼承 Foo.必要的話, 析構函數聲明爲 virtual. 如果你的類有虛函數, 則析構函數也應該爲虛函數. 注意 數據成員在任何情況下都必須是私有的.當重載一個虛函數, 在衍生類中把它明確的聲明爲 virtual. 理論依據: 如果省略 virtual 關鍵字, 代碼閱讀者不得不檢查所有父類, 以判斷該函數是否是虛函數.
3.8. 多重繼承
多重繼承允許子類擁有多個基類. 要將作爲 純接口 的基類和具有 實現 的基類區別開來.
相比單繼承 (見 繼承), 多重實現繼承可以複用更多的代碼.
真正需要用到多重 實現 繼承的情況少之又少. 多重實現繼承看上去是不錯的解決方案, 但你通常也可以找到一個更明確, 更清晰的不同解決方案.
只有當所有父類除第一個外都是 純接口類 時, 才允許使用多重繼承. 爲確保它們是純接口, 這些類必須以 Interface 爲後綴.
3.9. 接口
- 只有純虛函數 (“=0”) 和靜態函數 (除了下文提到的析構函數).
- 沒有非靜態數據成員.
- 沒有定義任何構造函數. 如果有, 也不能帶有參數, 並且必須爲 protected.
- 如果它是一個子類, 也只能從滿足上述條件並以 Interface 爲後綴的類繼承.
接口類不能被直接實例化, 因爲它聲明瞭純虛函數. 爲確保接口類的所有實現可被正確銷燬, 必須爲之聲明虛析構函數 (作爲上述第 1 條規則的特例, 析構函數不能是純虛函數). 具體細節可參考 Stroustrup 的 The C++ Programming Language, 3rd edition 第 12.4 節.
以 Interface 爲後綴可以提醒其他人不要爲該接口類增加函數實現或非靜態數據成員. 這一點對於 多重繼承 尤其重要. 另外, 對於 Java 程序員來說, 接口的概念已是深入人心.
Interface 後綴增加了類名長度, 爲閱讀和理解帶來不便. 同時,接口特性作爲實現細節不應暴露給用戶.
只有在滿足上述需要時, 類才以 Interface 結尾, 但反過來, 滿足上述需要的類未必一定以 Interface 結尾.
3.10. 運算符重載
一個類可以定義諸如 + 和 / 等運算符, 使其可以像內建類型一樣直接操作.
使代碼看上去更加直觀, 類表現的和內建類型 (如 int) 行爲一致. 重載運算符使 Equals(), Add()等函數名黯然失色. 爲了使一些模板函數正確工作, 你可能必須定義操作符.
雖然操作符重載令代碼更加直觀, 但也有一些不足:
- 混淆視聽, 讓你誤以爲一些耗時的操作和操作內建類型一樣輕巧.
- 更難定位重載運算符的調用點, 查找 Equals() 顯然比對應的 == 調用點要容易的多.
- 有的運算符可以對指針進行操作, 容易導致 bug. Foo + 4 做的是一件事, 而 &Foo + 4 可能做的是完全不同的另一件事. 對於二者, 編譯器都不會報錯, 使其很難調試;
重載還有令你吃驚的副作用. 比如, 重載了 operator& 的類不能被前置聲明.
一般不要重載運算符. 尤其是賦值操作 (operator=) 比較詭異, 應避免重載. 如果需要的話, 可以定義類似 Equals(), CopyFrom() 等函數.然而, 極少數情況下可能需要重載運算符以便與模板或 “標準” C++ 類互操作 (如 operator<<(ostream&, const T&)). 只有被證明是完全合理的才能重載, 但你還是要儘可能避免這樣做. 尤其是不要僅僅爲了在 STL 容器中用作鍵值就重載 operator== 或 operator<; 相反, 你應該在聲明容器的時候, 創建相等判斷和大小比較的仿函數類型.有些 STL 算法確實需要重載 operator== 時, 你可以這麼做, 記得別忘了在文檔中說明原因.
3.11. 存取控制
3.11. 聲明順序
- typedefs 和枚舉
- 常量
- 構造函數
- 析構函數
- 成員函數, 含靜態成員函數
- 數據成員, 含靜態數據成員
3.12. 編寫簡短函數
譯者 (YuleFox) 筆記
- 不在構造函數中做太多邏輯相關的初始化;
- 編譯器提供的默認構造函數不會對變量進行初始化, 如果定義了其他構造函數, 編譯器不再提供, 需要編碼者自行提供默認構造函數;
- 爲避免隱式轉換, 需將單參數構造函數聲明爲 explicit;
- 爲避免拷貝構造函數, 賦值操作的濫用和編譯器自動生成, 可將其聲明爲 private 且無需實現;
- 僅在作爲數據集合時使用 struct;
- 組合 > 實現繼承 > 接口繼承 > 私有繼承, 子類重載的虛函數也要聲明 virtual 關鍵字, 雖然編譯器允許不這樣做;
- 避免使用多重繼承, 使用時, 除一個基類含有實現外, 其他基類均爲純接口;
- 接口類類名以 Interface 爲後綴, 除提供帶實現的虛析構函數, 靜態成員函數外, 其他均爲純虛函數, 不定義非靜態數據成員, 不提供構造函數, 提供的話,聲明爲 protected;
- 爲降低複雜性, 儘量不重載操作符, 模板, 標準類中使用時提供文檔說明;
- 存取函數一般內聯在頭文件中;
- 聲明次序: public -> protected -> private;
- 函數體儘量短小, 緊湊, 功能單一;