《Effective C++》條款解讀

條款26:儘可能延後變量定義式的出現時間

  1. 一個變量被提前定義出來之後,後續可能因爲某些原因沒有被使用過,但是你仍得付出變量的構造成本和析構成本
  2. 延遲變量定義到非給他賦初值時,因爲構造之後再賦值的成本比初始化的成本高
  3. 如果變量在for循環中被使用,變量在循環外定義,在循環內賦值,調用1次構造函數和析構函數,n次賦值。變量在循環內初始化會調用n次構造函數和n次析構函數。除非賦值成本比構造+析構成本低,或者後續還需要用到這個變量,否則應該選擇第二種方案。

條款27:儘量少做轉型動作

  1. 轉型並非只是告訴編譯器把某種類型視爲另一種類型,什麼都沒做,而是真的編譯出運行時代碼。且對象的佈局方式和它們的地址計算方式隨編譯器的不同而不同。
  2. 轉型會讓我們寫出一些看似正確實際錯誤的代碼。比如說在自類中需要調用父類的方法,一種實現方式是,將this指向的對象強轉成父類對象,然後用.操作符調用父類方法,但是實際上改變的只是副本,而不是*this對象,正確的方法應該使用class::方法,或者virtual方法。
  3. dynamic_cast的許多實現版本執行速度相當慢。

條款28:避免返回handles指向對象內部成分

  1. handles包括對象的引用、指針和迭代器,表示用來取得某個對象的東西。
  2. 返回對象內部成員的handlers意味着外部函數可以改變內部數據,即使這個數據被聲明爲私有,這是一種破壞封裝性的行爲。如果非要返回內部成員,可以返回const &,使得該成員不被更改。
  3. 返回成員函數的handles同理,即使該函數是私有的,外部也可以調用。
  4. 可能導致dangling handles問題,即原先handles所指東西的所屬對象不復存在,因爲handles可能比其所指對象的壽命更長。

條款29:爲“異常安全”而努力是值得的

  1. 帶有異常安全性的函數不會泄露任何資源,不會造成數據破壞。反之,不具有異常安全性的函數可能會造成泄露資源,或者數據被破壞。
  2. 異常安全函數具有三個保證之一:基本保證,如果異常被拋出,程序依然保持在有效狀態,沒有對象或數據結構被破壞,但是程序的現實狀態不可預料,可能和異常前狀態一樣,也可能是其他有效狀態。強烈保證,要麼程序執行成功,要麼回滾至之前的狀態。不拋出異常保證,承諾程序絕不拋出異常。
  3. 往往可以通過“copy-and-swap”實現強烈保證,“copy-and-swap”指爲打算修改的對象創建一份副本,在副本上做出修改,如果異常拋出則返回源對象,如果執行成功則交換副本和源對象。但是因爲代碼中的短板效應,意味着系統的所有部分都要實現強烈保證,在現實上可能達不到。
  4. 如果有一個函數不具備異常安全性,整個系統就不具備異常安全性,所以函數提供的異常安全保證通常最高只是其他函數的異常安全保證的最弱等級。

條款30:透徹瞭解inlining的裏裏外外

  1. inline函數能避免函數調用帶來的開銷,且編譯器最優化機制通常都用來濃縮不含函數調用的代碼,所以編譯器因此有能力對函數本體執行最優化。
  2. inline可能會增加目標代碼的大小,過度熱衷inlining會造成程序體積太大,可能會導致額外的換頁行爲,降低指令高速緩存的命中率。
  3. inline只是對編譯器的申請,而不是強制命令。
  4. inline函數和templates函數通常被定義於頭文件中,因爲inlining在大多數程序中是編譯器行爲,爲了將一個“函數調用”替換爲“被調用函數的本體”,編譯器必須知道這個函數長什麼樣子。templates一旦被使用,編譯器爲了將它具體化,也需要知道它張什麼樣子。如果所有由該templates具體化得到的函數都應該inline,那可以將templates聲明爲inline,否則不應該。
  5. 不能將虛函數聲明爲inline,因爲virtual意味着直到運行時才確定調用哪個函數。
  6. 不能將構造函數和析構函數聲明爲inline,因爲即使一個空構造函數或析構函數,編譯器也會爲其填充構造成員變量或是父類對象,並且保證異常安全的代碼。聲明爲inline會導致這些代碼插入到其自類的相同位置。
  7. 不能調試inline函數。因爲inline函數被定義的地方並不是代碼執行的地方,真正被執行的地方是inline函數被展開的地方。

條款31:將文件間的編譯依存關係降至最低

  1. 當頭文件中接口和實現並未分離時,如果其中任何一處被改變,或者頭文件所依賴的其他頭文件有任何改變,那麼每個包含該頭文件的文件也必須重新編譯。
  2. 實現類的接口和實現的分離,應該將類分爲兩個類,一個定義接口,一個實現接口。在定義接口的類中包含指向實現接口類的指針成員,這種設計被稱爲pimpl idiom(pointer to implementation),客戶只include類的定義,而不知道具體實現,如此當接口實現變化時,並不會影響客戶編寫的代碼。
  3. 將聲明和定義放在不同的頭文件中。
  4. 實現接口和實現分離的一種方式是將類定義爲接口,其中通常不帶成員變量,也沒有構造函數,只有虛析構函數和純虛函數。由工廠函數或虛構造函數(靜態成員函數)創建該類的對象。
  5. 接口和實現分離會帶來額外的成本和開銷,可以考慮以漸進方式使用這項技術。

條款32:確定你的public繼承塑模出is-a關係

  1. public繼承意味着is-a,適用於base class的每一件事情一定也適用於derived class,因爲每一個derived class對象都是一個base class對象。(Liskov Substitution Principle)反之則不成立,並不是所有適用於子類的函數都適用於父類。
  2. 但是有時不嚴謹的繼承關係會打破這一原則。如在鳥類中定義函數fly,企鵝也是一種鳥,但是企鵝並不會飛。正方形是一種矩形,按說所有適用於矩形的操作也適用於正方形,但是矩形的長寬可以不等,正方形的長寬必須相等。
  3. 類之間除了is-a的關係,還有has-a和is-implemented-in-terms-of(根據某物實現出)的關係。

條款33:避免遮掩繼承而來的名稱

  1. 繼承中子類的作用域是被嵌套在父類的作用域內的,即子類會優先查找定義在自己內部的函數,查找不到函數名稱纔會去父類的作用域中查找。

  2. 當父類和子類中出現同名函數時(不管是non-virtual函數,還是virtual函數),定義在子類中的函數會遮掩在父類中定義的同名函數,即使該同名函數在父類中是重載函數,而且子類並沒有定義出全部重載函數,子類對象也不會調用父類中符合的函數,即子類把父類中所有的同名函數都屏蔽了。這違背了條款32,父類對象和子類對象並不是is-a的關係

  3. 在子類中使用using Base::fun;可以暴露出父類中定義的全部同名函數。

  4. 有些情況下你只希望暴露出父類定義的同名函數的一個或幾個(而不是全部),而又不想違背條款32,可以使用private繼承,還有轉交函數,轉交函數是在子類中定義一個你想暴露父類函數的同名函數(參數類型和返回類型也相同),在函數中調用Base::fun;

條款34:區分接口繼承和實現繼承

  1. 接口繼承和實現繼承不同,public繼承下總是會繼承接口。
  2. 聲明純虛函數的目的是讓子類只繼承函數接口,子類必須實現繼承來的純虛函數。從子類對象中調用父類的純虛函數是可以的,但是意義不大。
  3. 聲明虛函數的目的是讓子類繼承函數的接口和默認實現。但是當子類忘記實現自己的虛函數,使用父類的默認實現時,又可能造成風險,解決辦法是讓父類聲明純虛函數和一個默認實現函數,在子類重寫純虛函數時,調用父類的默認實現函數。也可以在父類外實現父類的純虛函數。
  4. 聲明非虛函數的目的是讓子類繼承接口和一份強制實現,它不該在子類中被重新定義,非虛函數的表示其不變性凌駕特異性。
  5. 兩個常見的錯誤,第一個是將所有的成員函數聲明爲non-virtual,第二個是將所有的成員函數聲明爲virtual。

條款35:考慮virtual函數以外的其他選擇

  1. 使用non-virtual interface(NVI),它是Template Method設計模式的一種特殊形式,以public non-virtual成員函數包裝降低訪問級別(private或protected)的virtual函數,前提是包裝器函數內的執行流程是穩定的,而具體步驟函數的實現是變化的,從而實現抽象不應該依賴實現細節,實現細節應該依賴抽象。
  2. 將virtual函數替換爲“函數指針成員變量”,這是Strategy設計模式的一種分解表現形式,在構造函數中接收函數指針,賦給函數指針成員變量。這樣做法的風險在於如果外部傳入函數依賴類內non-public方法,就會使得類要弱化封裝程度(提高訪問級別或聲明爲friend函數)。
  3. 使用function替換virtual函數,允許任何可調用物搭配一個兼容需求的簽名式(函數名稱,參數類型,返回類型),這也是Strategy設計模式的形式,在構造函數中接收一個可調用對象,包括普通函數指針,函數對象,成員函數指針。
  4. 將繼承體系的virtual函數替換爲另一個繼承體系的virtual函數,這是Strategy設計模式的傳統實現手法,在類中包含另一個類的成員變量,由這個類定義virtual函數,由他的子類繼承並定義實現出該函數。

條款36:絕不重新定義繼承而來的non-virtual函數

  1. 非虛函數是靜態綁定,虛函數是動態綁定,所以當一個子類對象的地址被賦值給父類指針和子類指針,通過這兩個指針訪問非虛函數,一個是定義在父類的方法,一個是定義在子類的方法,這就導致了二義性。
  2. 根據條款32:確定你的public繼承塑模出is-a關係,和條款34:區分接口繼承和實現繼承中非虛函數的不變性凌駕其特異性。所以你應該遵循此條款。

條款37:絕不重新定義繼承而來的缺省參數值

  1. 缺省參數值就是默認參數值,virtual函數是動態綁定(晚綁定),但是默認參數卻是靜態綁定(早綁定),如果子類在繼承到的虛函數中重新定義傳入的默認參數,並且創建了一個子類對象,這個對象中該函數的默認值是從父類中繼承得到的,而不是重新定義的。c++堅持這麼做的原因是爲了程序的執行速度和編譯器實現上的簡易度。
  2. 即使子類在繼承到的虛函數中填寫與父類相同的默認參數值,當父類的默認參數值改變時,子類的默認參數值也需要改變,這就帶來了重新定義繼承虛函數默認參數值的風險。

條款38:通過複合塑模出has-a或“根據某物實現出”

  1. public繼承表示是is-a的關係,複合表示是has-a或is-implemented-in-terms-of的關係。如果是可以抽象的真實世界存在的事物,如人、汽車、視頻畫面等,這些對象屬於應用域,而如果是軟件中的人工製品,如緩衝區、鎖、查找樹等,這些對象屬於應用域。當複合/組合發生在應用域內的對象則是has-a的關係,當發生在應用域則是is-implemented-in-terms-of的關係。
  2. 複合/組合關係和繼承關係完全不同,當繼承實現不了,或者違背原則時,應該考慮用組合來實現,並且組合是比繼承更好的關係。

條款39:明智而審慎地使用private繼承

  1. 如果類之間的繼承關係是private,編譯器不會自動將一個子類對象轉換爲一個父類對象,即不能使用父類指針接收子類對象。private繼承來的父類所有成員的屬性都會變成private,意味着只有實現部分被繼承,接口部分應略去。所以private繼承不表示子類和父類之間有什麼關係,而是你單純想複用父類中定義的函數。
  2. private繼承意味着implemented-in-terms-of(根據某物實現出),條款38也提到組合也產生這樣的關係,但是要儘可能使用組合,必要時才使用private繼承。

條款40:明智而審慎地使用多重繼承

  1. 多重繼承比單繼承複雜,當子類繼承的多餘一個類中定義了相同名稱的函數,就會導致歧義性,並且多重繼承很容易形成“鑽石型多重繼承”,即一個父類被兩個子類繼承,這兩個子類又同時被另一個類繼承。“鑽石型多重繼承”在默認情況下會使得最底層的子類對象擁有兩份最頂層父類對象的副本,要解決這個問題需要使用虛繼承。
  2. 虛繼承會增加對象的大小,增加對象初始化及賦值的複雜度,降低運行速度,如果最頂層父類不帶有任何數據情況會好一些。
  3. 多重繼承的確有其正當用途。

條款41:瞭解隱式接口和編譯期多態

  1. 對於模板類/函數,接口是隱式的,基於有效表達式。多態則是通過template具體化和函數重載解析發生在編譯期。

條款42:瞭解typename的雙重意義

  1. 模板內出現的類型如果是依賴於一個模板參數,稱之爲從屬類型,如果從屬類型在類內呈嵌套狀,我們稱它爲嵌套從屬類型。如果類型並不依賴模板參數,這樣的類型稱爲非從屬類型。默認情況下,嵌套從屬類型不是類型,除非你使用typename告訴編譯器它是類型,如typename C::iterator,typename Base<T>::Nested。但是在繼承時,和初始化列表裏不應該加typename

條款43:學習處理模板化基類內的名稱

  1. 在子類模板內通過this->使用父類模板內的成員類型,或者顯示寫出base::成員類型

條款44:將與參數無關的代碼抽離 templates

  1.  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章