《Effective C++》读书笔记(第二部分)
6. 繼承與面向對象設計(Inheritance and Object-Oriented Design)
條款32: 確定你的public 繼承塑模出is-a 關系
本條款告訴讀者一個非常基本的繼承思想:”public 繼承”意味is唱。適用于base classes 身上的每一件事情一定也適用于derived classes 身上,因為每一個derived class 對象也都是一個base class 對象,但反之不然。
條款33: 避免遮掩繼承而來的名稱
(1) derived classes 內的名稱會遮掩base classes 內的名稱。在public 繼承下從來沒有人希望如此。舉例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | class Base { ?private: ??int x; ?public: ??virtual void mfl() = 0; ??virtual void mfl(int); ??virtual void mf2(); ??void mf3 (); ??void mf3(double); }; class Derived: public Base { ?public: ??virtual void mfl(); ??void mf3 (); ??void mf4 (); ??… }; |
base class 內所有名為mfl 和mf3的函數都被derived class 內的mfl 和mf3函數遮掩掉了。從名稱查找觀點來看,Base: :mfl 和Base: :mf3 不再被Derived繼承!
(2) 為了讓被遮掩的名稱再見天日,可使用using 聲明式或轉變函數( forwarding
functions) 。舉例說明:
[1] 使用using聲明式
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | class Base { ?private: ??int x; ?public: ??virtual void mfl() = 0; ??virtual void mfl(int); ??virtual void mf2(); ??void mf3 (); } void mf3(double); class Derived: public Base { ?public: ??using Base::mfl; //使用using 聲明式 ??using Base: :mf3; //使用using 聲明式 ??virtual void mfl(); ??void mf3 (); ??void mf4(); } Derived d; int x; d.mf1 () ; //仍調用Derived: :mfl d.mf1 (x); //調用Base: :mfl d.mf2 () ; //調用Base: :mf2 d.mf3 ();//調用Derived: :mf3 d.mf3 (x); //調用Base: :mf3 |
[2] 使用轉變函數
| 1 2 3 4 5 6 7 8 9 | class Derived: private Base ( ?public: ??virtual void mfl () //轉變函數(forwading? function) , ??{ Base:: mfl ( );} //暗自成為inline } |
條款34: 區分接口繼承和實現繼承
本條款告訴程序員:
(1) 接口繼承和實現繼承不同。在public 繼承之下, derived classes 總是繼承base class
的接口。
(2) pure virtual 函數只具體指定接口繼承。(要求繼承者必須重新實現該接口)
(3) 簡樸的(非純) impure virtual 函數具體指定接口繼承及缺省實現繼承(繼承者可自己實現該接口也可使用缺省實現)。
(4) non-virtual 函數具體指定接口繼承以及強制性實現繼承。(繼承者必須使用該接口的實現)
舉例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Shape { ?public: ??virtual void draw( ) const = 0; //pure virtual 函數 ??virtual void error(const std::string& msg); //簡樸的(非純) impure virtual 函數 ??int objectID ( ) const;// non-virtual 函數 }; class Rectangle: public Shape { }; class Ellipse: public Shape { }; |
條款35: 考慮virtual函數以外的其他選擇
本條款告訴程序員,當需要使用virtual 函數時,可以考慮其他選擇。
Virtual函數的替代方案是:
(1) 使用non-virtual interface(NVI)手法。思想是:將virutal函數放在private中,而在public中使用一個non-virtual函數調用該virtual函數。優點是:用一個不能被子類重定義的函數,做一些預處理、后處理等,子類只需要在private中重新實現virtual函數即可。即:基類給出virtual函數的使用方法,而派生類給出virtual函數的使用方法。
舉例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | class GameCharacter { ?public: ??int healthValue() const{??????????????? // 1. 子類不能重定義 ????...?????????????????????????????? // 2. preprocess ????int retVal = doHealthValue();???? // 2. 真正的工作放到虛函數中 ????...?????????????????????????????? // 2. postprocess ???return retVal; ??} ??... ?private: ??virtual int doHealthValue() const {?? // 3. 子類可重定義 ????... ???} }; |
(2) 將virtual函數替換為“函數指針成員變量”(這是Strategy設計模式中的一種表現形式)。優點是對象實例和派生類對象,可使用各種實現,也可在運行時隨意改;缺點是:該函數不能訪問類中的私有成員
舉例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class GameCharacter; int defaultHealthCalc(const GameCharacter& gc); // default algorithm class GameCharacter { ?public: ??typedef int (*HealthCalcFunc)(const GameCharacter&); ??explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) ??: healthFunc(hcf) ??{} ??int healthValue() const { ???return healthFunc(*this); ??} ??... ?private: ??HealthCalcFunc healthFunc; }; |
(3) 以tr1::function成員變量替換virtual函數,這允許使用任何可調用物搭配一個兼容于需求的簽名式。這也是Strategy設計模式的某種形式。這種方式比上面的函數指針更靈活、限制更少:[1]返回值不一定是int,與其兼容即可; [2]可以是function對象; [3]可以是類的成員函數。
(4) 繼承體系內的virtual函數替換為另一個繼承體系內的virtual函數。這是Strategy設計模式的傳統實現手法。這種方式最大的優點是:可以隨時添加新的算法。舉例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | class GameCharacter; class HealthCalcFunc { ?public: ??... ??virtual int calc(const GameCharacter& gc) const ???{ ... } ??... }; HealthCalcFunc defaultHealthCalc; class GameCharacter { ?public: ??explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc) ???: pHealthCalc(phcf) ????{} ??int healthValue() const { ???return pHealthCalc->calc(*this); ?} ??... private: HealthCalcFunc *pHealthCalc; }; |
條款36: 絕對不要重新定義繼承而來的non-virtual函數
本條款告誡程序員:絕不要重新定義繼承而來的non-virtual函數,因為這不僅容易造成錯誤,而且是一種自相矛盾的設計。 舉例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class B{ ?public: ??void func(){ cout<<“B”;} }; class D:public B{ ?public: ??void func() { cout<<“D”;} }; |
下面是對B和D的使用:
| 1 2 3 4 5 | D dObject; B* basePtr = &dObject; D* dOjbectPtr = &dObject; |
看下面這兩種調用方式:
| 1 2 3 | basePtr->func(); dOjbectPtr->func(); |
你會發現打印結果為:
B
D
解釋:在C++繼承中,virtual函數是動態綁定的,調用的函數跟指針或者引用實際綁定的那個對象有關,而non-virtual函數是靜態綁定的,調用的函數只跟聲明的指針或者引用的類型相關。
此外,繼承者自己重新實現了non-virtual函數的行為是自相矛盾的。Non-virtual函數是用于同時指定函數接口和函數實現的,既然你想只繼承函數接口,就應該定義為non-virtual的。
條款37: 絕對不要重新定義繼承而來的缺省參數值
該條款告誡程序員:絕對不要重新定義一個繼承而來的缺省參數值,因為缺省參數值都是靜態綁定,而virtual函數-你唯一應該覆寫的東西-卻是動態綁定。
舉例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class Shape{ ?public: ??enum Color{RED,GREEN,BLUE}; ??virtual void draw(Color color = RED)const = 0; ??... }; class Circle:public Shape{ ?public: ??//竟然改變缺省參數值 ??virtual void draw(Color color = GREEN)const{ ... } }; Shape* pc = new Circle; pc->draw(); //注意調用的是: Circle::draw(RED),也就是說,此處的draw函數是基類和派生類的“混合物”。 |
為什么缺省參數是靜態綁定而不是動態綁定呢?主要原因是運行效率。如果是動態綁定,程序員使用起來很方便,但會降低運行效率,C++做了取舍,結果就是現在這樣。
條款38:XXXXXXXXXXXXXXXXXXX
條款39:明智而審慎地使用private繼承
(1)如果class之間的繼承關系是private。編譯器不會自動將一個derived class對象轉化為一個base class對象。由private base class繼承而來的所有成員,在derived class中都會變成private屬性,縱使它們在base class中原來是protected或public屬性。
(2)private繼承意味is-implemented-in-terms-of,它的級別比組合低,當derived class需要protected base class或者需重新定義繼承而來的virtual class時,設計才是合理的。
(3)與復合不同 ,private繼承可以使empty base空間最優化。舉例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class Empty{}; //empy class clsss HoldsAnyInt{ ?private: ??int x; ??Empty e; };//這個的大小為>sizeof(int),Empty空對象需要安插一個char到空對象,并且有齊位需求。 class HoldsAnyInt::private Empty{ ?private: ??int x; }; //這個sizeof大小為sizeof(int) 補充: class HoldsAnyInt::private Empty{ ?private: ??int cal() = 0; ??int x; }; //這個sizeof大小為8, 實際上為size(int) + sizeof(vptr) |
條款40:明智而審慎地使用多重繼承
(1) 多重繼承比單一繼承復雜。他可能導致新的歧義性,以及virtual繼承的需要
(2) Virtual繼承會增加大小、速度、初始化復雜度等等成本。如果virtual base classed不帶任何數據,將是最具使用價值的情況。
(3) 多重繼承最正當用途是:其中一個設計“public 繼承某個interface class”和“priavte繼承某個協助實現的class”的兩相結合。
7. 模板與泛型編程(Templates and Generic Programming)
條款41:了解隱式接口和編譯期多態
(1) class和templates都支持接口(interfaces)和多態(polymorphism)。
(2) 對classes而言接口是顯式的(explicit),以函數簽名為中心。多態則是通過virtual函數發生于運行期。
(3) 對template參數而言,接口是隱式的(implicit),奠基于有效表達式;多態則是通過template具現化和函數重載解析(function overloading resolution)發生于編譯期。
條款42:了解typename的雙重定義
(1) 聲明template參數時,前綴關鍵字class與typename可互換。
例如:
| 1 2 3 4 5 6 7 8 9 10 11 | template <class T> //or template <typename T> void swap(T& obj1, T& obj2) { ??T temp(obj1); ??obj1 = obj2; ??obj2 = temp; } |
(2) 請使用關鍵字typename標識嵌套從屬類型名稱;但不得在base class lists(基類列)或member initailization list(成員初值表列)內以作為base class修飾符。
例如,你必須:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | tempalte <typename C> void print2nd(const C& container)//打印容器內的第二元素 { ??if( containter.size() >= 2 ){ ???typename C::const_iterator iter( containter.begin() ); ???++iter; ???int value = *iter; ???std::cout << value; ??} } |
解釋:template內出現的名稱如果相依于某個template參數,稱之為從屬名稱;如果從屬名稱在class內呈嵌套狀,稱為嵌套從屬名稱。在上面的例子中,C::const_iterator就是嵌套從屬名稱。編譯器并不知道 const_iterator是個類型,除非你告訴編譯器,不然它以為這是C中的static成員變量或者是global變量。
但需要注意一下情況:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | template <typename T> class Derived:public Base<T>::Nested{ //base class list中不允許出現"typename" ?public: ??explicit Dervied(int x) : Base<T>::Nested(x){ //成員初始化列表中不允許"typename" ??typename Base<T>::Nested temp; //既不在base class list也不在初始化列表中,作為一個base class修飾符需加上typename. ??... ??} ??... }; |
條款43:學習處理模板化基類內的名稱
本條款給出了以下問題的解決方案:當基類是模板化的類時,派生類應該怎樣調用基類中的函數。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | template<typename Company> class MsgSender{ ?public: ??... ??void sendClear(const MsgInfo& info){ ???std::string msg; ???...//根據info產生信息 ???Company c; ???c.sendClearText(msg); ??} ??void sendSecret(const MsgInfo& info){...} //這里調用的是c.sendEncrypted. }; template <typename Company> class LoggingMsgSender:public MsgSender<Comany>{ ?public: ??... ??void sendClearMsg(const MsgInfo& info){ //為避免"名稱遮掩"現象的發生,采用了一個不同的名稱 ???...// record status information before sending message ???sendClear(info); ???...//record status information after sending message. ??} ??... }; |
以上代碼直接編譯會報錯:拋出了”sendClear不存在”的抱怨。解決方法有以下三個:
(1) 在base class函數調用動作之前加上”this->”:
| 1 2 3 4 5 6 7 8 9 10 11 | template <typename Company> void LoggingMsgSender<Company>::sendClearMsg(const MsgInfo& info){ ??... ??this->sendClear(info); //ok ??... } |
(2) 使用using聲明式:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | template <typename Company> class LoggingMsgSender:public MsgSender<Company>{ ?public: ??//這里的情況不是base class名稱被derived class名稱遮掩,而是編譯器不進入base base ??//作用域查找,于是我們通過using聲明式告訴它,請它這么做 ??using MsgSender<Company>::sendClear;//告訴編譯器,請它假設sendClear位于base class內 ??... ??void sendClearMsg(const MsgInfo& info){ ???... ???sendClear(info);//ok ???... ??} }; |
(3) 明明白白指出被調用函數位于base class內:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | template <typename Company> class LoggingMsgSender:public MsgSender<Company>{ ?public: ??... ??void sendClearMsg(const MsgInfo& info){ ??... ??MsgSender<Company>::sendClear(info); //ok ??... ?} ?... }; |
條款44:將與參數無關的代碼抽離template
(1) Templates生成多個classes和多個函數,所以任何template代碼都不該與某個造成膨脹的template參數產生相依關系。
(2) 因非類型模板參數而造成的代碼膨脹,往往可消除,做法是以函數參數或class成員變量替換template參數。
舉個例子,假設現在你要為固定尺寸的矩陣編寫一個template類,該類聲明要支持矩陣的逆運算,可以采用下面代碼:
| 1 2 3 4 5 6 7 8 9 10 11 | template <typename T, std::size_t n> //矩陣元素類型T,尺寸大小為n class SquareMatrix{ ?public: ???... ???void invert(); //逆運算 }; |
這樣定義,聲明以下兩個對象會產生不同的代碼,造成代碼膨脹:
| 1 2 3 | SquareMatrix<double,5> square1; SquareMatrix<double,10> square2; |
減小代碼膨脹的方法是采用以下定義:
| 1 2 3 4 5 6 7 8 9 10 11 | template <typename T > //矩陣元素類型T class SquareMatrix{ ?public: ??... ??void invert(std::size_t n); //把尺寸大小n作為參數 }; |
條款45:運用成員函數模板接受所有兼容類型
本條款告訴你,怎樣編寫成員函數模板。從下面例子說起:
怎樣支持以下操作:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | template <typename T> class SmartPtr{ ?public: ??explicit SmartPtr(T* realPtr);//智能指針通常以原始指針完成初始化 ??... }; SmartPtr<Top> top1_smart_ptr = SmartPtr<Middle>(new Middle); SmartPtr<Top> top2_smart_ptr = SmartPtr<Bottom>(new Bottom); SmartPtr<const Top> const_top2_ptr = top1_smart_ptr; |
一個比較好的方案是:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | //根據SmartPtr<U>創建一個SmartPtr<T>,其中T是基類,U是T的派生類 template <typename T> class SmartPtr{ ?public: ??template <typename U> ??SmartPtr(const SmartPtr<U>& other) :held_ptr_( other.get() ){...} //這里就完成子類向父類的隱式轉換過程. ??T* get()const{ return held_ptr_;} ??... ?private: ??T* held_ptr_; //這是SmartPtr持有的內置指針. }; |
上述中的SmartPtr構造函數便是成員函數模板(member function template),得出的結論是:
(1) 請使用member function template(成員函數模板)生成”可接受所有兼容類型”的函數。
(2) 如果你聲明member template用于”泛化copy構造”或”泛化assignment操作”,你還需要聲明正常copy構造函數和copy assignment操作符。(不然編譯器會為你生成默認的copy構造函數和copy assignment操作符)
條款46:需要類型轉換時請為模板定義非成員函數
本條款告訴程序員,當你需要進行類型轉化時,為了避免麻煩,最好將模板定義為非成員函數(如friend函數)。
條款47: 請使用traits classes 表現類型信息
條款48: 認識template 元編程
8. 定制new和delete(Customizing new and delete)
條款49: 了解new-handler 的行為
(1) set_new_handler 允許客戶指定一個函數,在內存分配無法獲得滿足時被調用。
(2) No-throw new 是一個頗為局限的工具,因為它只適用于內存分配;后繼的構造函數調用還是可能拋出bad_alloc異常。
條款50: 了解new 和delete 的合理替換時機
有許多理由需要寫個自定的口new 和delete ,包括改善效能、對heap 運用錯誤進
行調試、收集heap 使用信息。
條款51: 編寫new和delete時需固守常規
operator new內應該有一個無窮循環,并在其中嘗試分配內存,如果分配失敗,就調用new handler。它也應該有能力處理0 bytes申請(對于標準庫中的new操作符,當用戶申請0bytes,會返回1bytes的空間)。class版本還需要處理“比正確大小更大的(錯誤)申請”。
需要注意的是,operator new成員函數會被derived classes繼承,也就是說, base class的operator new可能被調用以分配derived class對象。因此 derived class的 operator new的代碼建議:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | struct Base{ ?static void* operator new(std::size_t size) throw( std::bad_alloc ); ?... }; struct Derived:public Base{...}; Derived* p = new Derived;//call Base::operator new. void* Base::operator new(std::size_t size) throw(std::bad_alloc) { ??if( size != sizeof(Base) ){ ??return ::operator new( size ); //call standard operator new version. } ... } |
條款52: 寫了placement new 也要寫placement delete
1. 當你寫一個placement new,請確定也寫出對應的placement delete。如果沒這樣做,你的程序可能會出現微弱時斷時續的內存泄漏;
2. 當你寫placement new和placement delete時,請確定不要無意識的(非故意的)遮掩了全局范圍默認的new/delete版本。
9. 雜項討論(Miscellany)
條款53: 不要輕忽編譯器的警告
條款54: 讓自己熟悉包括TR1在內的標準程序庫
本條款告訴程序員:
1.C++標準程序庫的主要機能由STL、iostreams、locales組成,并包括C99標準程序庫。
2.TR1添加了智能指針、一般化函數指針、hash-based容器、正則表達式,以及另外10個組件的支持。
3.TR1自身只是一個規范。為獲得tr1提供的好處,你需要一份實物。一個好的實物來源是boost。
條款55: 讓自己熟悉Boost
原創文章,轉載請注明:?轉載自董的博客
本文鏈接地址:?http://dongxicheng.org/cpp/effective-cpp-part2/
總結
以上是生活随笔為你收集整理的《Effective C++》读书笔记(第二部分)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《Effective C++》读书笔记(
- 下一篇: awk使用总结