讓我們開門見山的討論本話題:可以繼承的函數(shù)可以分為兩種:虛擬的和非虛擬的。然而,由于重定義一個派生的非虛函數(shù)始終是一個錯誤(參見條目36),因此我們可以放心地將此處的討論范圍縮小至以下情況:繼承一個含有默認(rèn)參數(shù)值的虛函數(shù)。
此情況下,證明本條目的結(jié)論非常簡單:虛函數(shù)是動態(tài)綁定的,而默認(rèn)參數(shù)值是靜態(tài)綁定的。
你說啥?靜態(tài)綁定與動態(tài)綁定之間的區(qū)別已經(jīng)讓你頭暈?zāi)垦A耍浚ū娝苤o態(tài)綁定又稱早期綁定,動態(tài)綁定又稱晚期綁定。)我們只好復(fù)習(xí)一下了。
一個對象的靜態(tài)類型就是你在對其進(jìn)行聲明時賦予它的類型。請考慮下面的類層次結(jié)構(gòu):
// 幾何形狀類
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
// 所有形狀必須提供一個自我繪制函數(shù)
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle: public Shape {
public:
// 請注意:默認(rèn)參數(shù)值變了——糟糕!
virtual void draw(ShapeColor color = Green) const;
...
};
class Circle: public Shape {
public:
virtual void draw(ShapeColor color) const;
...
};
用UML來表示:

現(xiàn)在請考慮下面的指針:
Shape *ps; // 靜態(tài)類型 = Shape*
Shape *pc = new Circle; // 靜態(tài)類型 = Shape*
Shape *pr = new Rectangle; // 靜態(tài)類型 = Shape*
示例中,由于ps,pc以及pr都聲明為指向Shape的指針,因此他們的靜態(tài)類型均為Shape*。請注意,這樣做使得無論他們實(shí)際指向的對象是什么類型,他們的靜態(tài)類型都必為Shape*。
對象的動態(tài)類型是通過他當(dāng)前引用的對象的類型決定的。也就是說,動態(tài)類型表明了他應(yīng)具有怎樣的行為。在上文的示例中,pc的動態(tài)類型是Circle*,pr的動態(tài)類型是Rectangle*。而對于ps來說,他在當(dāng)前根本不具備動態(tài)類型,因?yàn)樗壳埃┻€沒有引用任何對象呢。
動態(tài)類型,顧名思義,在程序運(yùn)行時可能會有所改變,通常是通過賦值操作發(fā)生:
ps = pc; // ps動態(tài)類型變?yōu)?/span>Circle*
ps = pr; // ps動態(tài)類型變?yōu)?/span>Rectangle*
虛函數(shù)是動態(tài)綁定的,這就意味著,對于一個特定的函數(shù)調(diào)用,其調(diào)用對象的動態(tài)類型將決定調(diào)用這一函數(shù)的哪個版本:
pc->draw(Shape::Red); // 調(diào)用 Circle::draw(Shape::Red)
pr->draw(Shape::Red); // 調(diào)用 Rectangle::draw(Shape::Red)
我知道這些都是老生常談了,你當(dāng)然已經(jīng)對虛函數(shù)有了透徹的理解。只有在虛函數(shù)包含默認(rèn)參數(shù)值時,情況才有所不同。這是因?yàn)椋ㄈ缟衔乃觯摵瘮?shù)是動態(tài)綁定的,但是默認(rèn)參數(shù)是靜態(tài)綁定的。這也就意味著對于一個虛函數(shù),你可能會調(diào)用它在派生類中的定義,而默認(rèn)參數(shù)值則采用基類中的值:
pr->draw(); // 調(diào)用 Rectangle::draw(Shape::Red)!
這種情況下,由于pr的動態(tài)類型是Rectangle*,于是此處便調(diào)用了虛函數(shù)draw的Rectangle版本,正如你所愿。在Rectangle::draw中,默認(rèn)參數(shù)值是Green。然而,因?yàn)?span style="font-family:"Courier New";">pr的靜態(tài)類型是Shape*,這里的draw調(diào)用將采用Shape類中的默認(rèn)參數(shù)值,而不是Rectangle!最終,在Shape類和Rectangle類之間,對于draw的調(diào)用必將出現(xiàn)混亂的無法預(yù)知的現(xiàn)象。
雖然ps,pc和pr是指針,但是并不影響上文的結(jié)論。如果它們是引用的話,問題同樣存在。這里只有一個重點(diǎn):draw是虛函數(shù),他的一個默認(rèn)參數(shù)值在派生類中被重定義了。
是什么讓C++在處理這一問題時如此不合常理? 答案是:運(yùn)行時效率。如果默認(rèn)參數(shù)值是動態(tài)綁定的話,那么編譯器必須提供一整套方案,為運(yùn)行時的虛函數(shù)參數(shù)確定恰當(dāng)?shù)哪J(rèn)值。而這樣做,比起C++當(dāng)前使用的編譯時決定機(jī)制而言,將會更復(fù)雜、更慢。魚和熊掌不可兼得,C++將設(shè)計的中心傾向了速度和簡潔,你在享受效率的快感的同時,如果你忽略本條目的建議,你就會陷入困惑。
一切看上去似乎盡善盡美了,但是一旦你不假思索的遵守本條建議,為基類和派生類分別提供默認(rèn)參數(shù)值的話,看看將會發(fā)生什么:
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;
...
};
吁……惱人的重復(fù)代碼。還有更糟的:這些重復(fù)代碼彼此還有依賴:如果Shape中的默認(rèn)參數(shù)值改變了的話,那么所有的派生類中相應(yīng)的值都必須改變。否則這些函數(shù)仍將改變繼承來的默認(rèn)參數(shù)值。那么怎么辦呢?
遇到麻煩了?虛函數(shù)無法按照你預(yù)想的方式運(yùn)行?這時候明智的做法是:考慮一個替代的設(shè)計方案,條目35中介紹了幾種虛函數(shù)的替代方案。其中一種是非虛擬接口慣用法方案(NVI慣用法):在基類中用一個公有的非虛函數(shù)調(diào)用一個私有的虛函數(shù),并在派生類中重定義這一虛函數(shù)。在這里,我們將默認(rèn)參數(shù)置于非虛函數(shù)中,讓虛函數(shù)做具體的工作。
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
void draw(ShapeColor color = Red) const
{ // 現(xiàn)在draw是非虛函數(shù)
doDraw(color); // 調(diào)用一個虛函數(shù)
}
...
private:
virtual void doDraw(ShapeColor color) const = 0;
// 這個函數(shù)做真正的工作
};
class Rectangle: public Shape {
public:
...
private:
virtual void doDraw(ShapeColor color) const;
//此處不需要默認(rèn)參數(shù)值
...
};
由于在派生類中不能對非虛函數(shù)進(jìn)行重載(參見條目36),因此,顯然地,這一設(shè)計方案使得draw函數(shù)中color參數(shù)的默認(rèn)值永遠(yuǎn)為Red。
時刻牢記
l 避免在對函數(shù)中繼承得來的默認(rèn)參數(shù)值進(jìn)行重定義,這是因?yàn)槟J(rèn)參數(shù)值是靜態(tài)綁定的,而虛函數(shù)(派生類中唯一的一系列可以重定義的函數(shù))是動態(tài)綁定的。