Guru of the Week 條款05:覆寫虛擬函式 (轉)

worldblog發表於2007-12-10
Guru of the Week 條款05:覆寫虛擬函式 (轉)[@more@] 

GotW #05 Overriding Virtual Functions

著者:Herb Sutter 

翻譯:kingofark

[宣告]:本文內容取自網站上的Guru of the Week欄目,其著作權歸原著者本人所有。譯者kingofark在未經原著者本人同意的情況下翻譯本文。本翻譯內容僅供自學和參考用,請所有閱讀過本文的人不要擅自轉載、傳播本翻譯內容;本翻譯內容的人請在閱讀瀏覽後,立即刪除其。譯者kingofark對違反上述兩條原則的人不負任何責任。特此宣告。

Revision 1.0

Guru of the Week 條款05:覆寫虛擬:namespace prefix = o ns = "urn:schemas--com::office" />

 

難度:6 / 10

 

(虛擬函式(virtual function)真是一個招人喜歡的基本特性,對嗎?好吧,如果你能回答下面這個問題,你就會發現她們有時真是冷若冰霜、寒氣刺骨。)

 

 

[問題]

 

當你在一個佈滿灰塵的角落翻尋公司的存檔程式碼時,你偶然中發現了無名氏編寫的一段如下的。這位無名氏程式設計師好像曾經試圖用這個程式做試驗,看看某些C++特性的運作狀況。你知道這個程式設計師希望程式執行後列印什麼結果嗎?你知道程式執行後的實際結果嗎?

 

  #include


  #include


  using namespace std;


 


  class Base {


  public:


  virtual void f( int ) {


  cout << "Base::f(int)" << endl;


  }


 


  virtual void f( double ) {


  cout << "Base::f(double)" << endl;


  }


 


  virtual void g( int i = 10 ) {


  cout << i << endl;


  }


  };


 


  class Derived: public Base {


  public:


  void f( complex ) {


  cout << "Derived::f(complex)" << endl;


   }


 


  void g( int i = 20 ) {


  cout << "Derived::g() " << i << endl;


  }


  };


 


  void main() {


  Base  b;


  Derived d;


  Base*  pb = new Derived;


 


  b.f(1.0);


  d.f(1.0);


  pb->f(1.0);


 


   b.g();


  d.g();


  pb->g();


 


  delete pb;


  }


 

 

[解答]

 

首先,我們談談編碼風格方面的問題:

 

1. void main()

 

這並不是一個合法的main宣告,儘管許多都允許這種寫法。應該使用“int main()”或者“int main(int argc, char* argv[])”。

 

然而,其實你仍然不需要新增任何返回語句(雖然有時加上返回語句是為了形成一種向外部者報告錯誤的良好編碼風格)。實際上,如果main沒有返回語句,其效果等同於了“return 0;”。

 

2. delete pb;

 

這看起來好像既無公害又無傷大雅——當然,前提是Base的編寫者提供了一個虛擬解構函式(virtual destructor)。然而,就像我們在這個程式中所看到的那樣,在沒有虛擬解構函式(virtual destructor)的情況下透過指向基類的指標進行刪除操作,這簡直就是邪惡的犯罪,同時也是幼稚和簡單的——如此以來,崩潰就是你所能期待的最好的事情了。

 

[規則]:把基類的解構函式(virtual destructor)宣告為virtual。

 

3. Derived::f(complex)

 

Derived並沒有過載(overload)Base::f,而是隱藏了它。這個細節非常重要,因為這意味著在Derived中,Base::f(int)和Base::f(double)是不可見的!(更何況某些流行的編譯器對此種情況甚至都不給出一個警告資訊。)

 

[規則]:當派生類中的函式與基類中的函式同名,而你又不想在派生類中隱藏這些基類函式的時候,請使用using宣告來把它們定置到可用範圍(pe)之內。

 

4. Derived::g(int i = 10)

 

除非你是故意要把別人搞糊塗,否則不要改變你所覆寫(overr)的繼承函式(inherited function)的預設引數。(一般來說,進行覆寫而不使用引數預設值並不是一個壞主意,但那是其本身的問題。)是的,這是一個合法合理的C++語句;不錯,其結果也被很好的定義了;但是,不,請不要這樣做。往下接著看,你會發現它到底是如何把人搞糊塗的。

 

[規則]:絕不要改變覆寫的繼承函式(overridden inherited function)

 

 

好了,讓我們不要再談那些編碼風格的瑣事了,現在我們就來看看主程式到底是不是按照那位無名氏程式設計師所期望的方式運作的。

 

  void main() {


  Base  b;


  Derived d;


  Base*  pb = new Derived;


 


*  b.f(1.0);


 

沒問題,呼叫Base::f(double)。

 

*  d.f(1.0);


 

這條語句呼叫Derived::f(complex)。為什麼?如前所述,Derived中沒有宣告“using Base::f;”,所以Base::f(int)和Base::f(double)都不能被呼叫。

 

無名氏先生也許原本是想要呼叫Base::f(double),但是此處他連一個編譯錯誤資訊也得不到,因為幸運的是(?),complex包含有一個從double的隱式轉換(*),因而編譯器把它看成是Derived::f(complex(1.0))。

 

*)注:在現有的C++標準草案中,轉換建構函式(conversion constructor)不是顯式的(explicit)。

 

*  pb->f(1.0);


 

有趣的事情在這裡發生了:雖然Base* pb指向一個Derived,但這個語句還是呼叫Base::f(double),因為過載解析過程(overload resolution)會以靜態型別(在這裡是指Base)完成,而不是以動態型別(在這裡是指Derived)完成。

 

*  b.g();


 

這條語句只是簡單的列印“10”,因為它引起對Base::g(int)的呼叫,其函式引數預設值為10。這沒什麼好奇怪的。

 

*  d.g();


 

這條語句列印“Derived::g()20”,因為它引起對Derived::g(int)的呼叫,其函式引數預設值為20。這也沒什麼好奇怪的。

 

*  pb->g();


 

這條語句列印“Derived::g()10”。這個結果也許會暫時死鎖你的精神剎車,導致你聲嘶力竭的進入一種精神停滯狀態——直到你認識到,編譯器的所作所為是再正常不過的了(雖然我們可憐的無名氏毫無疑問的應該被槍斃)。要記住,確定預設引數和過載一樣,是以物件的靜態型別(在這裡是指Base)完成的,因此選用了預設值10。然而,函式又恰好是virtual的,所以實際被呼叫的函式取決於物件的動態型別(在這裡是指Derived)。

 

最後,如果你能理解這剩下的幾個語句(儘管你會因此說:“噢嘔!”),那麼你就終於可以理解我開頭所說的“冷若冰霜、寒氣刺骨”的意思了。祝賀你!

 

*  delete pb;


  }


 

刪除操作,當然,會留下一些只被部分清除的東西,使變得不可捉摸……好好看看前面關於虛擬解構函式(virtual destructor)的敘述吧。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-990890/,如需轉載,請註明出處,否則將追究法律責任。

相關文章