避免對函式中繼承得來的預設引數值進行重定義
讓我們開門見山的討論本話題:繼承一個含有預設引數值的虛擬函式。
此情況下,本條目的證明問題則顯得十分了然:虛擬函式是動態繫結的,而預設引數值是靜態繫結的。
你說啥?靜態綁定於動態繫結之間的區別已經讓你頭暈目眩了?(靜態繫結又稱早期繫結,動態繫結又稱晚期繫結,這是官方說法。)我們只好複習一下了。
一個物件的靜態型別就是你在對其進行宣告時賦予它的型別。請考慮下面的類層次結構:
// 幾何形狀類 class Shape {public:
enum ShapeColor { Red, Green, Blue };
// 所有形狀必須提供一個自我繪製函式 virtual void draw(ShapeColor color = Red)
};
class Rectangle: public Shape {
public:
// 請注意:預設引數值變了 —— 糟糕! virtual void draw(ShapeColor color = Green) const;
};
class Circle: public Shape {
public:
virtual void draw(ShapeColor color) const;
};
用UML來表示:
現在請考慮下面的指標:
示例中,ps,pc以及pr都宣告為指向Shape的指標,因此他們的靜態型別均為Shape*。請注意,這樣做使得無論他們實際指向的物件是什麼型別,他們的靜態型別都必為Shape*。
物件的動態型別是通過他當前引用的物件的型別決定的。也就是說,動態型別表明了他應具有怎樣的行為。在上文的示例中,pc的動態型別是Circle*,pr的動態型別是Rectangle*。而對於ps來說,他在當前根本不具備動態型別,因為他(目前)還沒有引用任何物件呢。
動態型別,顧名思義,在程式執行時可能會有所改變,通常是通過賦值操作發生:
ps = pc; // ps當前的動態型別為Circle*ps = pr; // ps當前的動態型別為Rectangle*虛擬函式是動態繫結的,這就意味著,對於一個特定的函式呼叫,其呼叫物件的動態型別將決定呼叫這一函式的哪個版本:
pc->draw(Shape::Red); // 呼叫 Circle::draw(Shape::Red)pr->draw(Shape::Red); // 呼叫 Rectangle::draw(Shape::Red)我知道這些都是老生常談了,你當然已經對虛擬函式有了透徹的理解。只有在虛擬函式包含預設引數值時,情況才有所不同。這是因為(如上文所述),虛擬函式是動態繫結的,但是預設引數是靜態繫結的。這也就意味著對於一個虛擬函式,你可能會呼叫它在派生類中的定義,而預設引數值則採用基類中的值:
pr->draw(); // 呼叫 Rectangle::draw(Shape::Red)!這種情況下,由於pr的動態型別是Rectangle*,於是此處便呼叫了虛擬函式draw的Rectangle版本,正如你所願。在Rectangle::draw中,預設引數值是Green。然而,因為pr的靜態型別是Shape*,這裡的draw呼叫將採用Shape類中的預設引數值,而不是Rectangle!最終,在Shape類和Rectangle類之間,對於draw的呼叫必將出現混亂的無法預知的現象。
這裡ps,pc和pr是指標,然而這並不影響上文的結論。如果他們是引用的話,問題同樣存在。這裡只有一個重點:draw是虛擬函式,他的一個預設引數值在派生類中被重定義了。
為何C++在這一問題上如此倒行逆施? 答案是:執行時效率。如果預設引數值是動態繫結的話,那麼編譯器必須提供一整套方案,為執行時的虛擬函式引數確定恰當的預設值。而這樣做,比起C++當前使用的編譯時決定機制而言,將會更復雜、更慢。魚和熊掌不可兼得,C++將設計的中心傾向了速度和簡潔,你在享受效率的快感的同時,如果你忽略本條目的建議,你就會陷入困惑。
一切看上去似乎盡善盡美了,但是一旦你不假思索的遵守本條建議,為基類和派生類分別提供預設引數值的話,看看將會發生什麼:
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;
};
籲……惱人的重複程式碼。還有更糟的:這些重複程式碼彼此還有依賴:如果Shape中的默認引數值改變了的話,那麼所有的派生類中相應的值都必須改變。否則這些函式仍將改變繼承來的預設引數值。那麼怎麼辦呢?
遇到麻煩了?虛擬函式無法按照你預想的方式執行?這時候明智的做法是:考慮一個替代的設計方案,第35條中介紹了幾種虛擬函式的替代方案。其中一種是非虛擬介面慣例方案(NVI慣例):在基類中用一個公有的非虛擬函式呼叫一個私有的虛擬函式,並在派生類中重定義這一虛擬函式。在這裡,我們將預設引數置於非虛擬函式中,讓虛擬函式做具體的工作。
public:
enum ShapeColor { Red, Green, Blue };
void draw(ShapeColor color = Red) const // 現在draw是非虛擬函式 {
doDraw(color); // 呼叫一個虛擬函式 }
private:
virtual void doDraw(ShapeColor color) const = 0;
// 這個函式做真正的工作};
class Rectangle: public Shape {
public:
private:
virtual void doDraw(ShapeColor color) const; //此處不需要預設引數值
};
這一設計方案使得draw函式中color引數的預設值永遠為Red。
銘記在心
·避免在對函式中繼承得來的預設引數值進行重定義,這是因為預設引數值是靜態繫結的,而(派生類中唯一一類可以重定義的)虛擬函式是動態繫結的。