以1個例子來展開本條款論述內容。假定class D是class B的派生類,class B中有1個public成員函數mf:
class B{
public:
void mf();
……
};
class D: public B {……};
由1下方式調用
D x;
B* pB=&x;
pB->mf();
D* pD=&x;
pD->mf();
上面的兩次調用函數mf
得到的行動相同嗎?雖然mf
是個non-virtual函數,但是如果class D中有自己定義的mf
版本,那就行動真的不同。
class D: public B {
public:
void mf();//遮掩了B::mf。見條款33
……};
pB->mf();//調用B::mf
pD->mf();//調用D::mf)。
之所以行動不1致,是由于non-virtual函數是靜態綁定的(statically bound,條款 37)。pB被聲明為1個pointer-to-B,通過pB調用的non-virtual函數永久是B所定義的版本。但是virtual函數是動態綁定(dynamically bound,條款 37),所以virtual函數不受這個束縛,即通過指針調用,實際調用的函數是指針真正指向對象的那個函數。
如果你打算在class D中重新定義繼承自class B的non-virtual函數,D對象極可能會出現行動不1致行動。更明確1點,即任何1個D對象都可能表現出B或D的行動;決定因素不在對象本身,而在于“指向該對象之指針”當初聲明類型。References也會展現出和指針1樣難以理解的行動。
前面已說過,public繼承是is-a 關系(條款 32)。**條款**34說過,class內聲明1個non-virtual函數會為該class建立1個不變性(invariant),它凌駕其特異性(specialization)。將這兩個觀點實施到class B和class D上和non-virtual函數B::mf上,那末
現在在D中重新定義mf
,就會有矛盾。1、如果D真的有必要重新實現mf
(不同于B的),那末is-a 關系就不成立,由于每一個D都是B不再為真;既然這樣,就不應當以public情勢繼承。2、如果D必須以public方式繼承B,且D有需求實現不同的mf
,那末久不能反應出不變性凌駕特異性;既然這樣就應當聲明為virtual函數。3、如果每一個D是1個B為真,且mf
真的可以反應出不變性凌駕特異性的性質,那末D久不需要重新定義mf
了。
不論上面那個觀點,結論都相同:任何情況下都不應當重新定義1個基礎而來的non-virtual函數。
在條款 7已知道,base class內的析構函數應當是virtual;如果你違背了條款 7,你也就違背了本條款,由于析構函數每一個class都有,即便你沒有自己編寫。
總結
在繼承中,只能繼承兩種函數:virtual和non-virtual。在條款 36中我們學到,不能重新定義1個繼承而來的non-virtual函數。本條款討論的是繼承virtual函數問題,再具體1點:繼承1個帶有缺省參數值的virtual函數。
我們應當知道,virtual函數是動態綁定(dynamically bound),缺省參數值卻是靜態綁定(statically bound)。
對象的靜態類型(static type)是它在程序中被聲明時采取的類型,例如
class Shape{
public:
enum ShapeColor{ Red, Green, Blue};
virtual void draw(ShapeColor color=Red) const=0;
……
};
class Rectangle: public Shape{
public:
virtual void draw(ShapeColor color=Green) const;//不同缺省參數值,很糟
……
};
class Circle: public Shape{
public:
virtual void draw(ShapeColor color) const;
/*客戶調用上面函數時,如果使用對象調用,必須指定參數值,由于靜態綁定下這個函數不從base繼承缺省值。*/
/*如果使用指針或援用調用,可以不指定缺省參數值,動態綁定會從base繼承缺省參數值*/
……
};
這個繼承很簡單。現在這樣使用
Shape* ps;
Shape* pc=new Circle;
Shape* pr=new Rectangle;
這些指針類型都是pointer-to-Shape類型,都是靜態類型Shape*。對象的動態類型是指“目前所指對象類型”。動態類型可以表現出1個對象將會有甚么行動。pc動態類型是Circle*,pr動態類型是Rectangle*,ps沒有動態類型(它沒有指向任何對象)。動態類型可以在履行進程中改變,重新賦值可以改變動態類型。
virtual函數是動態綁定的,調用哪1份函數實現的代碼,取決于調用的那個對象的動態類型。
pc->draw(Shape::Red);
pr->draw(Shape::Red);
這樣調用無可非議,都帶有參數值。但是如果不帶參數值呢
pr->draw();//調用Rectangle::draw(Shape::Red)
上面調用中,pr動態類型是Rectangle*,所以調用Rectangle的virtual函數。Rectangle::draw函數缺省值是GREEN,但是pr是靜態類型Shape*,所以這個調用的缺省參數值來自Shape class,不是Rectangle class。這次調用兩個函數各出了1半的力。
C++之所以使用這么奇異的運作方式,是由于效力問題。如果缺省參數值動態綁定,編譯器必須有某種辦法在運行期為virtual函數決定適當的參數缺省值。這比目前實行的“在編譯器決定”的機制更慢且更復雜。為了履行速度和編譯器實現上的簡易度,C++做了這樣的取舍。
我們嘗試遵照這個規則,給base class和derived class提供相同參數值
class Shape{
public:
enum ShapeColor{ Red, Green, Blue};
virtual void draw(ShapeColor color=Red) const=0;
……
};
class Rectangle: public Shape{
public:
virtual void draw(ShapeColor color=Red) const;
……
};
這樣問題又來了,代碼重復且帶著相依性(with dependencies):如果Shape內缺省參數值改變了,那末derived classes的缺省參數值也要改變,否則就會致使重復定義1個繼承而來的缺省參數值。
當時如果的確需要derived classes的缺省參數值,那末就需要替換方法。條款 35列出了1些virtual函數的替換方法,例如NVI手法:
class Shape{
public:
enum ShapeColor{ Red, Green, Blue};
void draw(ShapeColor=Red) const
{
doDraw(color);
}
……
private:
virtual void doDraw(ShapeColor color) const=0;//真正在這里完成工作
};
class Rectangle: public Shape{
public:
……
private:
virtual void draw(ShapeColor color) const;
……
};
由于non-virtual函數不會被derived覆寫(條款 36),這個設計很清楚的使得draw函數的color缺省參數值總是Red。
總結