========================
Effective C++ 繼承與面向對象設計
書作者:Scott Meyers
原筆記作者:Justin
========================
Item 32 : public 繼承意味著 is-a 關系
--------------------------------------------------
tag: public inheritance 公有繼承 is-a
每一個類型為 Derived 的對象同時也是一個類型為 Base 的對象,反之不成立。
實際情況中很多“是一個”的體現并不那么純粹:大師說“鳥”都會飛,但是實際上是有不會飛的“鳥”的。
在公有繼承中,有兩種辦法來解決這種“不純粹”:
- 多重繼承。對于“鳥”的例子,設計一個“鳥”類,然后從中派生出一個“不會飛的鳥”類和一個“會飛的鳥”類,然后再在它們之中分別派生其他具體的“鳥”們。
- 允許運行時出錯。還是“鳥”的例子,對于每一個“鳥”類的派生類,不管它是不是能飛,都會有個“飛”的函數。不同的是,能飛的“鳥”就直接飛了,不能飛的“鳥”則會在“飛”函數里說:”對不起,我不能飛,找別人去吧……”(所謂的運行時錯誤,runtime error)
Item 33 : 避免遮掩繼承而來的名稱
--------------------------------------------------
tag: scopes
·derived classes 內的名稱會遮掩 base classes 內的名稱。
·可以使用 Using 聲明式或轉角函數 (forwarding functions)。
先在本地域中查找(local scope,比如說函數內部)是否有該名字的定義,如果沒有找到
往外一層名字域(比如說函數所在的類)中查找,如果沒有找到
再往外一層名字域(比如說函數所在類的父類)中查找,如果沒有找到
繼續忘外一層名字域中查找(比如說函數所在類的父類的父類,等等),一直找到全局名字域(global scope)還是沒找到的話,就報告錯誤。
在“洋蔥”的內部某層定義了和外部某層一樣名字的函數:使得位于內部的函數“屏蔽”了外部的同名函數(哪怕兩個函數擁有不同的參數表)。
第一,在公有繼承中,上述的情況是不允許存在的,因為從定義上來說,公有繼承中的子類應該具備父類所有的特征和功能,應該“是一個”父類。
第二,如果在上述情況中需要調用/訪問被“屏蔽”的函數/對象,有兩個方法可以采用:
using。用using“聲明”過完整的名字后,就可以“看見”并使用這個函數/對象了。
踢皮球函數(forwarding functions)。編寫一個函數,把真正的活踢給別人……
兩種方法示例見下,Derived_0是有“屏蔽”問題的類,Derived_1和Derived_2分別是采用了第一種和第二種方法的類。
class Base {
public :
virtual void func_1();
virtual void func_1( int param);
}
class Derived_0: public Base {
public :
virtual void func_1();
}
class Derived_1: public Base {
public :
using Base::func_1;
virtual void func_1();
}
class Derived_2: private Base {
public :
virtual void func_1();
virtual void func_1( int param)
{ Base::func_1(param);}
}
Item 34 : 區分接口繼承和實現繼承
--------------------------------------------------
tag: function interfaces, function implementations.
·聲明一個 pure virtual 函數以讓 derived classes 只繼承函數接口
·聲明 普通virtual 函數以讓 derived classes 繼承該函數的接口和缺省實現
·聲明 non-virtual 函數以令 derived classes 繼承函數的接口及一份強制性實現。
class AClass {
public :
virtual void interface_1() = 0 ;
virtual void interface_2()
{ /* the default implementation..*/ }
void interface_3()
{ /* the compulsory implementation..*/ }
// ..
} ;
class AClassDerived {
public :
virtual void interface_1()
{ /* OK you have to implement this..*/ }
virtual void interface_2()
{ /* you can, but don't have to implement this..*/ }
// void interface_3()
// {you can't implement your own..}
} ;
class AClass {
public :
virtual void interface_1. 5 () = 0 ;
protected :
void default_interface_1. 5 ()
{ /* ..*/ }
} ;
class AClassDerived {
public :
virtual void interface_1. 5 ()
{
// you can either do this
default_interface_1. 5 ();
// or implement in your own way..
}
} ;
Item 35 : 考慮 virtual 函數意外的其他選擇
--------------------------------------------------
tag: tr1::function tr1::bind (祥書上實例),
Template Method, Strategy ,
·使用 non-virtual interface(NVI)手法,即 Template Method 設計模式的一種特殊形式。以public non-virtual成員函數包裹較低訪問性的virtual函數
·將virtual函數替換為“函數指針成員變量”,即Strategy設計模式的一種分解表現形式
·以tr1::function成員變量替換virtual函數,因而允許使用任何可調用物(callable entiry)搭配一個兼容于需求的簽名式。也是Strategy設計模式的某種形式。
·將繼承體系內的 virtual函數替換為另一個繼承體系內的 virtual函數。即Strategy設計模式的傳統實現手法。
----------------------------------
一: Non-Virtual Interface 手法實現 Template Method 模式
:讓客戶通過 public non-virtual 成員函數間接調用 private virtual 函數,此即NVI手法,Template Method 設計模式的一種表現形式。
這個 public non-virtual 函數成為 virtual 函數的外覆器(wrapper)。
優點在于可在 wrapper 中做一些事前工作和事后工作。
NVI 手法中virtual function 不一定得是 private.
----------------------------------
二: Function Pointers 實現 Strategy 模式
:這種方法的實質,就是把接口函數的實現拿到了類之外。類之中只聲明接口的形式,只定義一個函數指針。真正干活的函數(實現)都不是類的成員。
這樣做帶來了一定的靈活性,具體采用哪種實現與類的繼承關系是獨立無關聯的;同時,非類成員函數也有局限性:無法訪問類的非公有成員。如果把函數定義為友元或利用公有函數輸出私有成員,又會破壞原設計的 封裝。如下代碼所示:
class AClass
{
public :
typedef void *(Interface)( /* param.. */ );
explicit AClass( Interface pint = defaultInterface) : pInterface(pint)
{}
private :
Interface pInterface;
} ;
在構造AClass對象的時候即可指定Interface的真身,雖然,它無法直接訪問AClass的非公有成員。
指針在C++里簡單一些,更推崇用對象(如智能指針tr1)來管理接口函數。(是不是想到item13?:))
原理和函數指針是一樣的,只不過因為用了對象來管理資源,使得應用更加靈活。當然,要付出更多一點的代碼體積和運行時間代價。
class AClass
{
// all are the same with the funtion pointer version
// except for:
typedef std::tr1::function < void ( /* param.. */ ) > Interface;
} ;
---------------------------------
三: 古典策略模式實現,也是我覺得比較漂亮且容易理解的實現方式。
用兩個類搞定:
class AInterface
{
public:
virtual void DoInterface(/* param.. */);
};
AInterface defaultInterface;
class AClass
{
public:
explicit AClass(AInterface * pinter = &defaultInterface) : pInter(pinter)
{}
void TryInterface()
{
pInter->DoInterface();
}
private:
pInterface * pInter;
};
Item 36 :絕不重新定義繼承而來的non-virtual函數
--------------------------------------------------
tag:
任何情況下都不該重新定義一個繼承而來的non-virtual函數,
Item 37 :絕不重新定義繼承而來的 缺省參數值
--------------------------------------------------
tag: 靜態類型(static type) 動態綁定 靜態綁定 前期綁定 后期綁定 bound bind
virtual 函數為動態綁定(dynamically bound),而缺省參數值卻是靜態綁定(statically bound)。
靜態類型 ( static type ):
在程序中被聲明時所采用的類型。
Shape* ps; //靜態類型為 Shape*, ps沒有動態類型,因為為指向任何對象。
Shape* pc = new Circle; //靜態類型為 Shape*, pc的動態類型為 Circle*
Shape* pr = new Rectangle; //靜態類型為 Shape*, pr的動態類型為 Rectangle*
動態類型 ( dynamic type ) :
目前所指對象的類型,
virtual 函數系動態綁定而來,調用一個virtual函數時,調用哪一份實現代碼,取決于發出調用的那個對象的動態類型。
class AClass
{
public :
virtual void func( int param = 123 )
{
// ..
}
} ;
class AClassDerived : public AClass
{
public :
// problematic overwriting the default parameter..
virtual void func( int param = 456 )
{
// ..
}
} ;
int main()
{
AClass * pA = new AClassDerived;
pA -> func();
}
由于函數默認參數的靜態綁定特性,pA->func()執行時param事實上被賦予了123,而非子類中期望的456,雖然接下來執行的是子類的函數實現……
C++考慮到執行效率和復雜性方面的代價,規定了只能是靜態綁定的。
解決方式:
可以用非虛函數接口(NVI)來解決這個問題,看代碼
class AClass
{
public:
void func(int param = 123)
{
funcImpl(param);
}
private:
virtual void funcImpl( int real_param ) = 0;
//..
};
class AClassDerived : public AClass
{
private:
virtual void funcImpl( int real_param )
{
//do whatever you feel like to do here..
}
//..
};
Item 38 :通過 composition 塑模出 has-a 或 is-implemented-in-terms-of
--------------------------------------------------
tag: has-a composition is-implemented-in-terms-of 、根據list實現set(祥書上例)
·復合(composition)的意義同public繼承完全不同。
·在應用域(application domain), 復合意味著 has-a,
在實現域(implementation domain),復合意味著 is-implemented-in-terms-of
composition是類型之間的一種關系,即某種類型的對象內含它種類型的對象。
composition、 layering分層、 containment內含、aggregation聚合、 embedding內嵌
composition意味著has-a 或 is-implemented-in-terms-of。
Item 39 :明智而審慎地使用 private 繼承
--------------------------------------------------
tag: private inheritance
Private inheritance意味著 implemented-in-terms-of.只有實現部分被繼承,接口部分被略去。
若 D 以 private 形式繼承 B, 意思是 D 對象根據 B 對象實現而得。
公有繼承中的子類對象是可以被轉換為它的父類對象的(“是一個”的關系),而私有繼承中這種轉換是不成立的。
另外一點,私有繼承中父類的所有公有和保護成員(public和protected)到了子類中,都變成了私有成員。
因為上面的特性,私有繼承并不滿足“是一個”模型的需要。更可憐的是,私有繼承并不能代表一種設計思路(公有繼承代表了“是一個”的模型設計),而僅僅是“有一個”模型的一種實現手段(私有繼承過來的所有成員都是私有的,從這個角度來說它就只是“實現”)。
另一種手段大師在Item38中有提過,就是用類成員的方式來構造,名曰composition。
既然兩者都能實現“有一個”模型,那么如何選擇呢?能用composition就用composition,必需私有繼承的時候方才私有繼承。
比如我們有個AClass:class AClass{
public:
virtual void Interface_1(/*..*/);
};
以下為私有繼承:class BClass : private AClass{
private:
virtual void Interface_1(/*..*/);
//..
};
而下面的composition可以達到一樣甚至更好的效果:class AnotherAClass: public AClass{
public:
virtual void Interface_1(/*..*/);
//..
};
class DClass{
private:
AnotherAClass* a;
//..
};
BClass和DClass都實現了“有一個”,但相比之下還是能分辨出長短:
DClass中的AnotherAClass是私有成員,除了它自己沒有人能夠訪問修改;而私有繼承的BClass不能保證其“擁有”的AClass實現部分不會被第三者修改,即使是私有繼承來的。(為什么這么說?看下去……)
BClass私有繼承了AClass,相當于它“有了一個”AClass可以用,可以玩。AClass中的公有/保護成員都變成了BClass的人,但是在享受使用這些成員的同時,BClass還要承擔提供這些成員給別人服務的義務。
ITEM35中曾經提到:虛擬函數機制和公有/私有/保護體制是沒有任何關系的。因此在例子中的Interface_1有可能在以下的情況中被替代然后“調用”:
一個CClass公有繼承了BClass
CClass定義了自己的Interface_1版本
有一個BClass的指針,指向一個CClass的對象,某個操作中調用了Interface_1(CClass的實現版本)
這時候BClass可能要有意見了:它的作者并沒有打算讓它的繼承者修改BClass版本的Interface_1,但事實是CClass違背了它的意志!
很曲折哈?希望我下次讀的時候還能看懂@#¥%
DClass由于只是定義了一個指向AnotherAClass的指針,那么在定義DClass的文件中就不需要include AClass或AnotherAClass的頭文件。于是就避免了編譯依賴(compilation dependecies)
而BClass因為是繼承了AClass,在BClass的文件中就需要加上AClass的頭文件,也就不可避免的產生了編譯時的依賴。
由此看來,絕大部分情況下,組合方式(composition)是要優于私有繼承的。之所以說“絕大部分”,是因為大師說了:
對于EBO(Empty Base Optimization)的情況,私有繼承就顯現出了它的優勢。
所謂EBO就是這樣的一種情況,有一種特殊的類,它沒有非靜態數據成員(non-static data member),也沒有虛函數(于是不會需要空間存儲虛表)。
所以這樣的一種類其實不占用任何空間,不過因為C++不允許0字節的對象存在,而且很多編譯器都會添加額外的空間來實現字節對齊,于是這種特殊的類的實際大小應該是1個char對象的大小。
在這種類中,往往會有很多typedef,enum,靜態數據成員或者是非虛函數。所以他們還是有價值的。
需要在“有一個”關系中利用這種類的時候,如果采用composition,那么根據上面的結論,就需要付出額外的空間來“存放”這個本來不占空間的類。
然而如果是私有繼承呢,就可以避免這種情況。
Item 40 :明智而審慎地使用多重繼承
--------------------------------------------------
tag: Multiple Inheritance, MI
MI 的優與劣。
MI 的第一個問題就是名字沖突, 最經典的例子就是鉆石問題 (diamond problem)。
設想 A 中有一個函數叫做 GetName(), B 和 C 中都將有這一函數成員,這個時候 D::GetName() 的真正實現是來自 B 的還是 C 的呢?二義性出現了 (ambiguity) 。
不過如果真的發生了這種情況,要解決的方法也不是沒有,可以這樣做:
D d;
d.B::GetName(); //Calling B's implementation
另外一個高階一點的方法叫做虛繼承 (virtual inheritance) 。對于在虛擬繼承中的父類,其中的成員都保證不會在后面的子類中出現二義現象 (ambiguity) 。
class A
{
public:
void GetName();
};
class B : virtual public A { };
class C : virtual public A { };
class D : public B, public C { }
D d;
d.GetName(); //there is no ambiguity here.
但是虛繼承不是沒有代價的,大師說這種技術會使得最終代碼變得更大,訪問虛擬繼承中的父類成員也會變得更慢一些。
這個也不難理解。和空間換時間一樣,和不給牛吃草牛就不干活一樣。 ( 另外的一個代價我還沒能完全理解透徹:書上說因為虛繼承中基類的初始化是由繼承關系中最底層的子類負責的,因此對于這些最底下的 “ 嫡孫 ” 類來說,就不是那么方便了 )
于是大師建議只有在必要的時候才使用虛繼承,而在虛繼承中的基類里也不要放置數據成員,這樣就不用擔心初始化的問題了。
不過存在就是合理,還是有需要用到 MI 的時候。一個在書中提到的使用 MI 的情形是:當需要從一個類 AClass 中繼承接口,又需要從另外一個類 BClass 中繼承實現細節時,就可以考慮在公有繼承 AClass 的同時又私有繼承 BClass 。道理大致就是這樣,就不編造程序畫蛇添足了。
總結一下: MI 比 SI(Single Inheritance) 要復雜容易出錯 ( 比如說鉆石問題 ) ,即使可以用虛繼承來解決鉆石問題,但是其帶來的代碼體積增大,訪問效率下降以及初始化問題還是不能忽視的。最后話說回來,需要用到 MI 的時候,小心點用便是
posted on 2010-03-15 22:54
Euan 閱讀(431)
評論(0) 編輯 收藏 引用 所屬分類:
C/C++