程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 避免對派生的非虛函數進行重定義

避免對派生的非虛函數進行重定義

編輯:關於C++

今天無意中發現一個關於C++基礎的問題,當時愣是沒理解是什麼原因,現在搞明白了,就寫下來了 。先看小程序,先實踐再理論吧,要不大家就睡著了。

#include <iostream>
using namespace std;
class Base    
{    
public:    
      virtual void funtion(int arg = 1){cout<<arg<<endl;} 
};
class Derive : public Base
{
public:    
      virtual void funtion(){cout<<"Derive"<<endl;}
      virtual void funtion(int arg){cout<<"Derive"<<arg<<endl;}
};    
int main(int argc, char *argv[])
{    
    Base* obj = new Derive();
    obj->funtion();    
    system("pause");    
    return 0;        
}

上面的程序會出現什麼結果呢?我想會有很多人看到這個地方就會懷疑我程序的正確性了, 大呼“你的程序是錯的”,但真的錯嗎?我們可以先執行下程序看下結果。很明顯,結果是調用了子類 的函數,並且子類中arg參數的值是父類中的值1,運行結果為“Derive 1”。

下面我就解釋下 這種結果的原因,首先我先要說下對象的兩種類型:動態類型和靜態類型。

靜態類型:指針或 者是引用聲明時的類型。

動態類型:由他實際指向的類型確定。

例如:

Base 

*pgo=   //pgo靜態類型是Base *    
new Derive; //動態類型是Derive *    
Asterioid *pa = new Asterioid; //pa的靜態類型是 Asterioid *    
//動態類型也是 Asterioid *    
pgo = pa; //pgo靜態類型總指向Base *    
//動態類型指向了 Asterioid *    
Base &rgo = *pa; //rgo的靜態類型是Base    
//動態類型是 Asterioid

虛函數是動態綁定的,而默認參數值是靜態綁定的。運行時效率。 如果默認參數值是動態綁定的話,那麼編譯器必須提供一整套方案,為運行時的虛函數參數確定恰當的 默認值。而這樣做,比起C++當前使用的編譯時決定機制而言,將會更復雜、更慢。魚和熊掌不可兼得 ,C++將設計的中心傾向了速度和簡潔,你在享受效率的快感的同時,如果你忽略本條目的建議,你就 會陷入困惑。

其實對於這個問題在[Effective C++第3版]中也有提到,其第36條:避免對派生 的非虛函數進行重定義。下面看下書中的描述:

現在考慮以下的層次結構:B是一個基類,D是 由B的公有繼承類,B類中定義了一個公有成員函數mf,由於這裡mf的參數和返回值不是討論的重點,因 此假設mf是無參數無返回值的函數。也就是說:

class B {    
public:    
  void mf();    
};    
class D: public B {  };    
即使不知道B、D、mf的任何信息,讓我們聲明一個D的對象x:    
D x;                           // x 是D類型的對象    
B *pB = &x;                       // 指向x的指針    
pB->mf();                         // 通過指針調用mf函數    
D *pD = &x;                       // 指向x的指針    
pD->mf();                         // 通過指針調用mf函數

在這裡,如果告訴你pD- >mf()與pB->mf()可能擁有不同的行為,你一定會感到意外。這也難怪:因為兩次都是在調用x對 象的成員函數mf,因為兩種情況下都是用了同一函數和同一對象,mf()理所應當應該有一致的行為。難 道不是嗎?

你說得沒錯,的確“理所應當”。但這一點無法得到保證。在特殊情況下,如果mf 是非虛函數並且D類中對mf進行了重定義,那麼問題就出現了:

class D: public B {    
public:    
  void mf();                   // 隱藏了B::mf; 參見第33條    
};    
pB->mf();                         // 調用B::mf    
pD->mf();                         // 調用D::mf

此類“雙面行為”的出現,究其原因 ,是由於諸如B::mf和D::mf這樣的非虛函數是靜態綁定的(參見第37條)。這也就意味著:由於我們將 pB聲明為指向B的指針,那麼通過pB所調用的所有非虛函數都將調用B類中的版本,即使pB指向一個B的 派生類的對象也是如此,正如上文示例所示。

然而,對於虛函數而言,它們在編譯期間采用動 態綁定(再次參見第37條),因此它們不會被這個問題困擾。如果mf是虛函數,那麼無論通過pB還是pD 來調用mf都會是對D::mf的調用,這是因為pB和pD實際上指向同一對象,這個對象是D類型的。

如果你正在編寫D類,並且你對由B類繼承而來的mf函數進行了重定義,那麼D類將會表現出不穩定的行 為。在特定情況下,任意給定的D對象在調用mf函數時可能表現出B或D兩種不同的行為,而且決定哪種 行為的因素是指向mf的指針的類型,與對象本身沒有任何關系。引用同指針一樣會出現這種莫名其妙的 行為。

但是,本文的內容僅僅是從實際角度出發做出的分析,我知道,你真正需要的是對“避 免對派生的非虛函數進行重定義”這一命題的理論推導。我很樂意效勞。

第32條解釋了公有繼 承意味著A是一個B,第34條描述了為什麼在類中聲明一個非虛函數是對類本身設置的“個性化壁壘”。 將上述理論應用到類B、D和非虛你函數B::mf上,我們可以得到:

·對B生效的所有東西對D也生 效,這是因為所有的D對象都是B對象。

·繼承自B的類必須同時繼承mf的接口和實現,這是因 為mf是B類中的非虛函數。

現在,如果在D類中對mf進行了重定義,那麼你的設計方案中就出現 了一個矛盾。如果D確實需要與B不同的mf實現方案,並且對於所有的B對象,無論這些對象多麼個性化 ,它們都必須使用B實現版本的mf,於是我們可以很簡單地的出以下的結論:並不是每個D都是一個B。 這種情況下,D並非公有繼承自B。然而,如果我們確實需要D是B的公有繼承類的話,並且D確實需要與B 不同的mf實現版本,那麼mf對B的“個性化壁壘”作用就不復存在了。這種情況下,mf應該是虛函數。 最後,如果每個D確實是一個B,並且mf確實對B起到了“個性化壁壘”的作用,那麼D中並不會真正的重 定義mf,它也不應該做出這樣的嘗試。

無論從哪個角度講,我們都必須無條件地禁止對派生的 非虛函數進行重定義。

如果閱讀本文給你一種似曾相識的感覺,那麼你一定是對閱讀過的第7條 還有印象,在那裡,我們解釋了為什麼多態基類的析構函數必須為虛函數。如果你違背了第7條的思想 (比如,你在多態基類中聲明了一個非虛析構函數),那麼你也就同時違背了本條的思想。這是因為在 派生類中繼承到的非虛函數一定會被重定義。即使派生類中不聲明任何析構函數也是如此,這是因為, 對於一些特定的函數,即使你不自己生成它們,編譯器也會自動為你生成它們(參見第5條)。從本質 上講,第7條只不過是本條的一個特殊情況,只是因為它十分重要,我們才把它單列出一條來。

銘記在心

·避免在派生類中重定義非虛函數。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved