Guru of the Week #5:虛擬函式的重新定義 (轉)

worldblog發表於2007-12-04
Guru of the Week #5:虛擬函式的重新定義 (轉)[@more@]

作者:Hub Sutter
譯者:plpliuly

/*此文是譯者出於自娛翻譯的GotW(Guru of the Week)系列文章第5篇,原文的版權是屬於Hub Sutter(著名的C++專家,"Exceptional C++"的作者)。此文的翻譯沒有徵得原作者的同意,只供學習討論。——譯者
*/

#5 虛的重新定義 (1997年5月14提出)
難度: 6/10

  虛擬函式是C++的一個基本特性,對不對?如果你能夠回答下面的問題,那說明你確實已經掌握.

問題:
  假設你正在看一些很老的原始碼,其中有一段如下的程式碼,作者已經不知其人了.這個作者寫這段程式碼好像僅僅是為了驗證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類提供了虛擬解構函式才會沒事.否則,透過一個沒有虛擬解構函式的基類的指標來釋放繼承類的是危險的,因為,你能得到的結果就是出錯.(譯者注:至少是洩漏)
  [原則]將基類的解構函式定義為虛擬函式

  3.Derived::f( complex )

  Derived類並沒有過載Base::f...它僅僅是覆蓋了它們.此處的區別是非常重要的,因為這意味著Base::f(int)和Base::f(double)對Derived類是不可見的!(注意:某些流行的編譯器甚至對此連警告錯誤都不給.)
  [原則]當你在繼承類中定義一個與基類中函式同名的函式,如果你不想將基類中的函式隱藏掉,你必須用"using"指示語句來將基類中的函式納入繼承類的pe中來,以便對繼承類可見.

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

  除非你真的想讓別人迷糊,否則不要在繼承類中改變函式的引數預設值.(一般來講,儘量採用函式過載來代替引數預設值的改變是一個不錯的方法.)當然,這是合法的C++語句,結果也是可預知的,但是,不要這樣做.你可以看看下面的討論中這樣做會怎樣的讓別人迷惑.
  [原則]決不要改變基類中函式的引數預設值.

  討論完幾個主要的程式碼風格問題後,讓我們切入正題來看看程式是不是會如程式碼作者所希望的那樣執行:

  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 )不會被呼叫.
  程式碼作者也許希望呼叫後者,但是此處連一個編譯錯誤都不會給出.comlex有一個對於double型別的隱式轉換(*),因此編譯器將上面的語句看作是Derived::f( complex(1.0) ).
 
  (*)根據目前的C++標準草案,這個轉換建構函式不是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.然而,此處的函式碰巧是虛擬函式,因此實際被呼叫的函式是由物件的動態型別(此處是Derived)決定的.
 
  如果你完全理解了上面的最後幾段解釋,你也就理解了我們今天討論的問題.祝賀你.
 
  delete pb;
  }
  此處的delete顯然會讓物件只是部分的釋放,導致記憶體錯誤...看看上面討論的虛擬建構函式就明白了.
----
(結束)


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

相關文章