讓我們開門見山的討論本話題:可以繼承的函數可以分為兩種:虛擬的和非虛擬的。然而,由于重定義一個派生的非虛函數始終是一個錯誤(參見條目36),因此我們可以放心地將此處的討論范圍縮小至以下情況:繼承一個含有默認參數值的虛函數。
此情況下,證明本條目的結論非常簡單:虛函數是動態綁定的,而默認參數值是靜態綁定的。
你說啥?靜態綁定與動態綁定之間的區別已經讓你頭暈目眩了?(眾所周知,靜態綁定又稱早期綁定,動態綁定又稱晚期綁定。)我們只好復習一下了。
一個對象的靜態類型就是你在對其進行聲明時賦予它的類型。請考慮下面的類層次結構:
// 幾何形狀類
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;
...
};
用UML來表示:

現在請考慮下面的指針:
Shape *ps; // 靜態類型 = Shape*
Shape *pc = new Circle; // 靜態類型 = Shape*
Shape *pr = new Rectangle; // 靜態類型 = Shape*
示例中,由于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慣用法):在基類中用一個公有的非虛函數調用一個私有的虛函數,并在派生類中重定義這一虛函數。在這里,我們將默認參數置于非虛函數中,讓虛函數做具體的工作。
class Shape {
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;
//此處不需要默認參數值
...
};
由于在派生類中不能對非虛函數進行重載(參見條目36),因此,顯然地,這一設計方案使得draw函數中color參數的默認值永遠為Red。
時刻牢記
l 避免在對函數中繼承得來的默認參數值進行重定義,這是因為默認參數值是靜態綁定的,而虛函數(派生類中唯一的一系列可以重定義的函數)是動態綁定的。