1. 程式人生 > >避免對函式中繼承得來的預設引數值進行重定義

避免對函式中繼承得來的預設引數值進行重定義

讓我們開門見山的討論本話題:繼承一個含有預設引數值的虛擬函式。

此情況下,本條目的證明問題則顯得十分了然:虛擬函式是動態繫結的,而預設引數值是靜態繫結的。

你說啥?靜態綁定於動態繫結之間的區別已經讓你頭暈目眩了?(靜態繫結又稱早期繫結,動態繫結又稱晚期繫結,這是官方說法。)我們只好複習一下了。

一個物件的靜態型別就是你在對其進行宣告時賦予它的型別。請考慮下面的類層次結構: 

// 幾何形狀類 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*

 示例中,pspc以及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*,於是此處便呼叫了虛擬函式drawRectangle版本,正如你所願。在Rectangle::draw中,預設引數值是Green。然而,因為pr的靜態型別是Shape*,這裡的draw呼叫將採用Shape類中的預設引數值,而不是Rectangle!最終,在Shape類和Rectangle類之間,對於draw的呼叫必將出現混亂的無法預知的現象。

這裡pspcpr是指標,然而這並不影響上文的結論。如果他們是引用的話,問題同樣存在。這裡只有一個重點: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//此處不需要預設引數值  
};

 這一設計方案使得draw函式中color引數的預設值永遠為Red

銘記在心

·避免在對函式中繼承得來的預設引數值進行重定義,這是因為預設引數值是靜態繫結的,而(派生類中唯一一類可以重定義的)虛擬函式是動態繫結的。