Effective C++ - 繼承與面向物件設計
author: lunar
date: Wed 16 Sep 2020 03:14:38 PM CST
6. 繼承和麵向物件程式設計
條款32: Make sure public inheritance models "is-a".
如果你令class D
以public形式繼承class B
, 你便是告訴編譯器每一個型別為D的物件同時也是一個型別為B的物件.
"public繼承"意味著is-a, 適用於base class的每一件事也要適用於derived class身上.
條款33: Avoid hiding inherited names.
這是一個關於作用域的問題. 當編譯器處理一個變數時, 它會現在local作用域內查詢該變數.
對於繼承了base class和derived class, 其作用域巢狀在base class內. 對於在derived class的函式mf4
中呼叫的一個函式mf2
, 編譯器會首先在mf4
的作用域內查詢. 然後在derived class內查詢, 然後去base class內查詢. 然後在包含base class的namespace內查詢. 最後在global作用域內查詢.
當子類需要過載父類的成員函式時, 則需要過載所有的同名函式. 如果只是過載一個, 則其它同名函式將會被覆蓋, 無法被子類繼承. 即使引數不同也一樣.
這類問題的解決辦法可以藉助using
關鍵字來完成, 例如下面的程式碼, 子類就可以正常使用父類的方法.
#include <iostream> using namespace std; class Base { public: void func(int x) { cout << "base func" << endl; } }; class Derived: public Base { public: using Base::func; void func() { cout << "derived func" << endl; } }; int main() { Derived d; d.func(1); return 0; }
條款34: Differentiate between inheritance of interface and inheritance of implementation.
對於base class的成員函式的宣告和定義, 我們希望有多種不同的繼承方式:
- 只繼承base class的函式宣告: 將相應介面設為純虛擬函式可以實現.
- 同時繼承函式宣告和定義: 使用非純虛擬函式可以實現
條款35:Consider alternatives to virtual functions.
藉由 Non-Virtual Interface 手法實現 Template Method 模式
有這樣一種場景:對於所有動物的父類Animal,所有動物都要定義“吃”這個動作,但是每個子類的實現方式又有不同。我們需要在每次呼叫“吃”的API時都要列印一次日誌,這個日誌對於所有不同實現的子類都是相同的。
我們可以這樣實現:將具體吃的實現函式定義為private virtual
型別的函式,而日誌列印的函式放在一個wrapper
函式裡面,這個函式是non-virtual的,在這個函式裡可以呼叫具體的實現函式。
這種手法稱為non-virtual interface (NVI)
藉由 Function Pointers 實現 Strategy 模式
利用函式指標將進行實際操作,在建構函式中將這個函式指標傳入。
藉由 tr1::function 完成 Strategy 模式
std::tr1::function 是一種通用、多型的函式封裝,tr1::function的例項可以對任何可呼叫的物件進行儲存、複製和呼叫操作。
在例項化時,其接受一個函式的型別作為模板引數。比如對於接受一個const int
,返回int
的函式來說:
std::tr1::function<int (const int)> instance;
可以將這個instance
指向任何可呼叫物件的地址,然後就可以像普通函式一樣進行呼叫。
這個條款看不下去了,看得人頭大。
條款36:Never define an inherited non-virtual function.
這個是很顯而易見的,如果你想要過載父類的函式的話,最好還是使用virtual函式。因為non-virtual函式都是靜態繫結的,你用哪種型別的變數呼叫成員函式,就只會呼叫該種類型的成員函式,不會管你物件具體是哪種型別的。
條款37:Never redefine a function's inherited default parameter value.
由於條款36,只需要考慮要不要重定義繼承而來的帶有預設引數值的virtual函式。
這種情況下,本條款成立的理由是:virtual函式系動態繫結,而預設引數值是靜態繫結的。
也就是說,對於Base* b = new Derived()
類似形式定義的變數,當b呼叫函式時,函式的具體實現雖然會使用Derived
類裡面定義的,但是預設引數卻會使用Base
中定義的。
如果實在想要為函式帶一個預設引數,可以考慮採用條款35所提到的NVI 風格的寫法。
條款38:Model "has-a" or "is-implemented-in-terms-of" through composition.
中文意思是:通過複合塑模出 has-a 或 “根據某物實現出”。
複合是型別之間的一種關係,當某種型別的物件內含其它型別的物件,便是這種關係。
程式中的物件其實相當於你所塑造的世界中的某些事物,例如人、汽車、一幀幀視訊畫面等等,這樣的物件屬於應用域(application domain)。另一些物件則完全是實現細節上的人工製品,比如互斥鎖,二叉樹,緩衝區等等,這類物件屬於實現域(implementation domain)。
應用域的物件之間的關係則是has-a,而實現域之間的關係則是“根據某物實現出”。例如,你要實現一個自己版本的set,你想通過STL的list 來實現,但是你不能直接繼承list,因為前面的條款講過:直接public繼承會導致兩個物件之間的關係變為is-a。所以只能在底層用list來裝載資料,就如同list底層通過陣列來裝載資料。這樣兩者之間的關係就是“根據某物實現出”。
條款39: Use private inheritance judiciously.
private繼承意味著兩個主要的規則:
- 編譯器不會自動將derived class物件轉換為base class物件;
- 由private class繼承而來的所有成員,在derived class中都會變成private屬性,即使它們在base class中屬於protected甚至public屬性。
private繼承意味著 implemented-in-terms-of 的關係。如果你讓class D private繼承class B,你的用意是為了採用class B內已經備妥的特性,而不是B物件和D物件存在任何觀念上的關係。
複合的意義也是這樣,儘可能使用複合,只有當protected成員或者virtual函式牽扯進來的時候才使用private繼承。
給個示例:
一個Widget型別在實現中需要使用到Timer的某個函式並重寫一次,想要重寫就只能繼承。重寫時放在private領域內,因為這是內部實現時要用到的介面,不是可以對外開放的介面。
還有一種寫法,在Widget內宣告一個巢狀式private class WidgetTimer
,後者以public的形式繼承Timer
。
有兩點理由證明下面這種寫法更好:
Widget
類可能會被別的類繼續繼承,這樣這個virtual函式又可能會被重寫。而下面的寫法就不存在這種問題,因為private領域內定義的類是不對子類可見的。- 編譯依存性的問題,如果
Widget
繼承自Timer
,則勢必會和Timer
產生編譯依存性。而後者可以選擇將WidgetTimer
移出,然後帶一個簡單的WidgetTimer
宣告式。
條款40:Use multiple inheritance judiciously.
C++其實並不建議使用多重繼承。
如果你繼承的類也繼承了其它的類的話,這時來自於基類的資料會同時存在你的父類和本類中,造成了空間的浪費。這時可以使用虛繼承。
對於虛繼承的建議:
- 非必要不適用;
- 如果要使用virtual base classes,極可能避免在其中放置資料。