決不要重新定義繼承而來的預設參數值
讓我們從一開始就把問題簡化。預設參數只能作為函數的一部分而存在;另外,只有兩種函數可以繼承:虛函數和非虛函數。因此,重定義預設參數值的唯一方法是重定義一個繼承而來的函數。然而,重定義繼承而來的非虛函數是一種錯誤(參見條款37),所以,我們完全可以把討論的範圍縮小為 "繼承一個有預設參數值的虛函數" 的情況。
既然如此,本條款的理由就變得非常明顯:虛函數是動態綁定而預設參數值是靜態繫結的。
什麼意思?你可能會說你不懂這些最新的物件導向術語;或者,過度勞累的你一時想不起靜態和動態綁定的區別。那麼,讓我們來複習一下。
對象的靜態類型是指你聲明的存在於程式碼文本中的類型。看下面這個類階層:
enum ShapeColor { RED, GREEN, BLUE };
// 一個表示幾何形狀的類
class Shape {
public:
// 所有的形狀都要提供一個函數繪製它們本身
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;
...
};
用圖形來表示是下面這樣:
Shape
//
/ /
/ /
Rectangle Circle
現在看看這些指標:
Shape *ps; // 靜態類型 = Shape*
Shape *pc = new Circle; // 靜態類型 = Shape*
Shape *pr = new Rectangle; // 靜態類型 = Shape*
這個例子中, ps, pc,和pr都被聲明為Shape指標類型,所以它們都以此作為自己的靜態類型。注意,這和它們真的所指向的對象的類型絕對沒有關係 ---- 它們的靜態類型總是Shape*。
對象的動態類型是由它當前所指的對象的類型決定的。即,對象的動態類型表示它將執行何種行為。上面的例子中,pc的動態類型是Circle*,pr的動態類型是Rectangle*。至於ps,實際上沒有動態類型,因為它(還)沒有指向任何對象。
動態類型,顧名思義,可以在程式運行時改變,典型的方法是通過賦值:
ps = pc; // ps的動態類型
// 現在是Circle*
ps = pr; // ps的動態類型
// 現在是Rectangle*
虛函數是動態綁定的,意思是說,虛函數通過哪個對象被調用,具體被調用的函數就由那個對象的動態類型決定:
pc->draw(RED); // 調用Circle::draw(RED)
pr->draw(RED); // 調用Rectangle::draw(RED)
我知道這些都是老掉牙的知識了,你當然也瞭解虛函數。(如果想知道它們是怎麼實現的,參見條款M24)但是,將虛函數和預設參數值結合起來分析就會產生問題,因為,如上所述,虛函數是動態綁定的,但預設參數是靜態繫結的。這意味著你最終可能調用的是一個定義在衍生類別,但使用了基類中的預設參數值的虛函數:
pr->draw(); // 調用Rectangle::draw(RED)!
這種情況下,pr的動態類型是Rectangle*,所以Rectangle的虛函數被調用 ---- 正如我們所期望的那樣。Rectangle::draw中,預設參數值是GREEN。但是,由於pr的靜態類型是Shape*,這個函數調用的參數值是從Shape類中取得的,而不是Rectangle類!所以結果將十分奇怪並且出人意料,因為這個調用包含了Shape和Rectangle類中Draw的聲明的組合。你當然不希望自己的軟體以這種方式運行啦;至少,使用者不希望這樣,相信我。
不用說,ps, pc,和pr都是指標的事實和產生問題的原因無關。如果它們是引用,問題也會繼續存在。問題僅僅出在,draw是一個虛函數,並且它的一個預設參數在子類中被重新定義了。
為什麼C++堅持這種有違常規的做法呢?答案和運行效率有關。如果預設參數值被動態綁定,編譯器就必須想辦法為虛函數在運行時確定合適的預設值,這將比現在採用的在編譯階段確定預設值的機制更慢更複雜。做出這種選擇是想求得速度上的提高和實現上的簡便,所以大家現在才能感受得到程式啟動並執行高效;當然,如果忽視了本條款的建議,就會帶來混亂。