更新時間:2018-11-29 來源:黑馬程序員技術(shù)社區(qū) 瀏覽量:
3.1. 構(gòu)造函數(shù)的職責(zé)
不要在構(gòu)造函數(shù)中進行復(fù)雜的初始化 (尤其是那些有可能失敗或者需要調(diào)用虛函數(shù)的初始化).
定義:
在構(gòu)造函數(shù)體中進行初始化操作.
優(yōu)點:
排版方便, 無需擔(dān)心類是否已經(jīng)初始化.
缺點:
在構(gòu)造函數(shù)中執(zhí)行操作引起的問題有:
結(jié)論:
構(gòu)造函數(shù)不得調(diào)用虛函數(shù), 或嘗試報告一個非致命錯誤. 如果對象需要進行有意義的 (non-trivial) 初始化, 考慮使用明確的 Init() 方法或使用工廠模式.
如果類中定義了成員變量, 則必須在類中為每個類提供初始化函數(shù)或定義一個構(gòu)造函數(shù). 若未聲明構(gòu)造函數(shù), 則編譯器會生成一個默認的構(gòu)造函數(shù), 這有可能導(dǎo)致某些成員未被初始化或被初始化為不恰當(dāng)?shù)闹?
定義:
new 一個不帶參數(shù)的類對象時, 會調(diào)用這個類的默認構(gòu)造函數(shù). 用 new[] 創(chuàng)建數(shù)組時, 默認構(gòu)造函數(shù)則總是被調(diào)用. 在類成員里面進行初始化是指聲明一個成員變量的時候使用一個結(jié)構(gòu)例如 int _count = 17 或者 string _name{"abc"} 來替代 int _count 或者 string _name 這樣的形式.
優(yōu)點:
用戶定義的默認構(gòu)造函數(shù)將在沒有提供初始化操作時將對象初始化. 這樣就保證了對象在被構(gòu)造之時就處于一個有效且可用的狀態(tài), 同時保證了對象在被創(chuàng)建時就處于一個顯然”不可能”的狀態(tài), 以此幫助調(diào)試.
缺點:
對代碼編寫者來說, 這是多余的工作.
如果一個成員變量在聲明時初始化又在構(gòu)造函數(shù)中初始化, 有可能造成混亂, 因為構(gòu)造函數(shù)中的值會覆蓋掉聲明中的值.
結(jié)論:
簡單的初始化用類成員初始化完成, 尤其是當(dāng)一個成員變量要在多個構(gòu)造函數(shù)里用相同的方式初始化的時候.
如果你的類中有成員變量沒有在類里面進行初始化, 而且沒有提供其它構(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ù).
對單個參數(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.
除單參數(shù)構(gòu)造函數(shù)外, 這一規(guī)則也適用于除第一個參數(shù)以外的其他參數(shù)都具有默認參數(shù)的構(gòu)造函數(shù), 例如 Foo::Foo(string name, int id = 42).
優(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)在注釋中明確說明.
最后, 只有 std::initializer_list 的構(gòu)造函數(shù)可以是非 explicit, 以允許你的類型結(jié)構(gòu)可以使用列表初始化的方式進行賦值. 例如:
如果你的類型需要, 就讓它們支持拷貝 / 移動. 否則, 就把隱式產(chǎn)生的拷貝和移動函數(shù)禁用.
定義:
可拷貝類型允許對象在初始化時得到來自相同類型的另一對象的值, 或在賦值時被賦予相同類型的另一對象的值, 同時不改變源對象的值. 對于用戶定義的類型, 拷貝操作一般通過拷貝構(gòu)造函數(shù)與拷貝賦值操作符定義. string 類型就是一個可拷貝類型的例子.
可移動類型允許對象在初始化時得到來自相同類型的臨時對象的值, 或在賦值時被賦予相同類型的臨時對象的值 (因此所有可拷貝對象也是可移動的). std::unique_ptr<int> 就是一個可移動但不可復(fù)制的對象的例子. 對于用戶定義的類型, 移動操作一般是通過移動構(gòu)造函數(shù)和移動賦值操作符實現(xiàn)的.
拷貝 / 移動構(gòu)造函數(shù)在某些情況下會被編譯器隱式調(diào)用. 例如, 通過傳值的方式傳遞對象.
優(yōu)點:
可移動及可拷貝類型的對象可以通過傳值的方式進行傳遞或者返回, 這使得 API 更簡單, 更安全也更通用. 與傳指針和引用不同, 這樣的傳遞不會造成所有權(quán), 生命周期, 可變性等方面的混亂, 也就沒必要在協(xié)議中予以明確. 這同時也防止了客戶端與實現(xiàn)在非作用域內(nèi)的交互, 使得它們更容易被理解與維護. 這樣的對象可以和需要傳值操作的通用 API 一起使用, 例如大多數(shù)容器.
拷貝 / 移動構(gòu)造函數(shù)與賦值操作一般來說要比它們的各種替代方案, 比如 Clone(), CopyFrom() or Swap(), 更容易定義, 因為它們能通過編譯器產(chǎn)生, 無論是隱式的還是通過 = 默認. 這種方式很簡潔, 也保證所有數(shù)據(jù)成員都會被復(fù)制. 拷貝與移動構(gòu)造函數(shù)一般也更高效, 因為它們不需要堆的分配或者是單獨的初始化和賦值步驟, 同時, 對于類似省略不必要的拷貝這樣的優(yōu)化它們也更加合適.
移動操作允許隱式且高效地將源數(shù)據(jù)轉(zhuǎn)移出右值對象. 這有時能讓代碼風(fēng)格更加清晰.
缺點:
許多類型都不需要拷貝, 為它們提供拷貝操作會讓人迷惑, 也顯得荒謬而不合理. 為基類提供拷貝 / 賦值操作是有害的, 因為在使用它們時會造成對象切割. 默認的或者隨意的拷貝操作實現(xiàn)可能是不正確的, 這往往導(dǎo)致令人困惑并且難以診斷出的錯誤.
結(jié)論:
如果需要就讓你的類型可拷貝 / 可移動. 作為一個經(jīng)驗法則, 如果對于你的用戶來說這個拷貝操作不是一眼就能看出來的, 那就不要把類型設(shè)置為可拷貝. 如果讓類型可拷貝, 一定要同時給出拷貝構(gòu)造函數(shù)和賦值操作的定義. 如果讓類型可拷貝, 同時移動操作的效率高于拷貝操作, 那么就把移動的兩個操作 (移動構(gòu)造函數(shù)和賦值操作) 也給出定義. 如果類型不可拷貝, 但是移動操作的正確性對用戶顯然可見, 那么把這個類型設(shè)置為只可移動并定義移動的兩個操作.
建議通過 = default 定義拷貝和移動操作. 定義非默認的移動操作目前需要異常. 時刻記得檢測默認操作的正確性. 由于存在對象切割的風(fēng)險, 不要為任何有可能有派生類的對象提供賦值操作或者拷貝 / 移動構(gòu)造函數(shù) (當(dāng)然也不要繼承有這樣的成員函數(shù)的類). 如果你的基類需要可復(fù)制屬性, 請?zhí)峁┮粋€ public virtual Clone() 和一個 protected 的拷貝構(gòu)造函數(shù)以供派生類實現(xiàn).
如果你的類不需要拷貝 / 移動操作, 請顯式地通過 = delete 或其他手段禁用之.
在能夠減少重復(fù)代碼的情況下使用委派和繼承構(gòu)造函數(shù).
定義:
委派和繼承構(gòu)造函數(shù)是由 C++11 引進為了減少構(gòu)造函數(shù)重復(fù)代碼而開發(fā)的兩種不同的特性. 通過特殊的初始化列表語法, 委派構(gòu)造函數(shù)允許類的一個構(gòu)造函數(shù)調(diào)用其他的構(gòu)造函數(shù). 例如:
繼承構(gòu)造函數(shù)允許派生類直接調(diào)用基類的構(gòu)造函數(shù), 一如繼承基類的其他成員函數(shù), 而無需重新聲明. 當(dāng)基類擁有多個構(gòu)造函數(shù)時這一功能尤其有用. 例如:
如果派生類的構(gòu)造函數(shù)只是調(diào)用基類的構(gòu)造函數(shù)而沒有其他行為時, 這一功能特別有用.
優(yōu)點:
委派和繼承構(gòu)造函數(shù)可以減少冗余代碼, 提高可讀性. 委派構(gòu)造函數(shù)對 Java 程序員來說并不陌生.
缺點:
使用輔助函數(shù)可以預(yù)估出委派構(gòu)造函數(shù)的行為. 如果派生類和基類相比引入了新的成員變量, 繼承構(gòu)造函數(shù)就會讓人迷惑, 因為基類并不知道這些新的成員變量的存在.
結(jié)論:
只在能夠減少冗余代碼, 提高可讀性的前提下使用委派和繼承構(gòu)造函數(shù). 如果派生類有新的成員變量, 那么使用繼承構(gòu)造函數(shù)時要小心. 如果在派生類中對成員變量使用了類內(nèi)部初始化的話, 繼承構(gòu)造函數(shù)還是適用的.
僅當(dāng)只有數(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ù)功能. 并且存取功能是通過直接訪問位域, 而非函數(shù)調(diào)用. 除了構(gòu)造函數(shù), 析構(gòu)函數(shù), Initialize(), Reset(), Validate() 等類似的函數(shù)外, 不能提供其它功能的函數(shù).
如果需要更多的函數(shù)功能, class 更適合. 如果拿不準(zhǔn), 就用 class.
為了和 STL 保持一致, 對于仿函數(shù)和 trait 特性可以不用 class 而是使用 struct.
注意: 類和結(jié)構(gòu)體的成員變量使用不同的命名規(guī)則.
使用組合 (composition, YuleFox 注: 這一點也是 GoF 在 <<Design Patterns>> 里反復(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ù).
真正需要用到多重實現(xiàn)繼承的情況少之又少. 只在以下情況我們才允許多重繼承: 最多只有一個基類是非抽象類; 其它基類都是以 Interface 為后綴的 純接口類.
定義:
多重繼承允許子類擁有多個基類. 要將作為 純接口 的基類和具有 實現(xiàn) 的基類區(qū)別開來.
優(yōu)點:
相比單繼承 (見 繼承), 多重實現(xiàn)繼承可以復(fù)用更多的代碼.
缺點:
真正需要用到多重 實現(xiàn) 繼承的情況少之又少. 多重實現(xiàn)繼承看上去是不錯的解決方案, 但你通常也可以找到一個更明確, 更清晰的不同解決方案.
結(jié)論:
只有當(dāng)所有父類除第一個外都是 純接口類 時, 才允許使用多重繼承. 為確保它們是純接口, 這些類必須以 Interface 為后綴.
關(guān)于該規(guī)則, Windows 下有個 特例.
接口是指滿足特定條件的類, 這些類以 Interface 為后綴 (不強制).
定義:
當(dāng)一個類滿足以下要求時, 稱之為純接口:
接口類不能被直接實例化, 因為它聲明了純虛函數(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é)不應(yīng)暴露給用戶.
結(jié)論:
只有在滿足上述需要時, 類才以 Interface 結(jié)尾, 但反過來, 滿足上述需要的類未必一定以 Interface 結(jié)尾.
除少數(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)用點要容易的多.有的運算符可以對指針進行操作, 容易導(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ù)重載.
將 所有 數(shù)據(jù)成員聲明為 private, 并根據(jù)需要提供相應(yīng)的存取函數(shù). 例如, 某個名為 foo_ 的變量, 其取值函數(shù)是 foo(). 還可能需要一個賦值函數(shù) set_foo().
特例是, 靜態(tài)常量數(shù)據(jù)成員 (一般寫做 kFoo) 不需要是私有成員.
一般在頭文件中把存取函數(shù)定義成內(nèi)聯(lián)函數(shù).
參考 繼承 和 函數(shù)命名
在類中使用特定的聲明順序: 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ù)成員友元聲明應(yīng)該放在 private 區(qū)段. 如果用宏 DISALLOW_COPY_AND_ASSIGN 禁用拷貝和賦值, 應(yīng)當(dāng)將其置于 private 區(qū)段的末尾, 也即整個類聲明的末尾. 參見可拷貝類型和可移動類型.
.cc 文件中函數(shù)的定義應(yīng)盡可能和聲明順序一致.
不要在類定義中內(nèi)聯(lián)大型函數(shù). 通常, 只有那些沒有特別意義或性能要求高, 并且是比較短小的函數(shù)才能被定義為內(nèi)聯(lián)函數(shù). 更多細節(jié)參考 內(nèi)聯(lián)函數(shù).
傾向編寫簡短, 凝練的函數(shù).
我們承認長函數(shù)有時是合理的, 因此并不硬性限制函數(shù)的長度. 如果函數(shù)超過 40 行, 可以思索一下能不能在不影響程序結(jié)構(gòu)的前提下對其進行分割.
即使一個長函數(shù)現(xiàn)在工作的非常好, 一旦有人對其修改, 有可能出現(xiàn)新的問題. 甚至導(dǎo)致難以發(fā)現(xiàn)的 bug. 使函數(shù)盡量簡短, 便于他人閱讀和修改代碼.
在處理代碼時, 你可能會發(fā)現(xiàn)復(fù)雜的長函數(shù). 不要害怕修改現(xiàn)有代碼: 如果證實這些代碼使用 / 調(diào)試困難, 或者你需要使用其中的一小段代碼, 考慮將其分割為更加簡短并易于管理的若干函數(shù).
首發(fā): http://java.itheima.com