避免對派生的非虛擬函式進行重定義
今天無意中發現一個關於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條只不過是本條的一個特殊情況,只是因為它十分重要,我們才把它單列出一條來。
銘記在心
·避免在派生類中重定義非虛擬函式。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69976881/viewspace-2730588/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- C++ 派生類函式過載與虛擬函式繼承詳解C++函式繼承
- 虛擬函式,虛擬函式表函式
- 虛擬函式 純虛擬函式函式
- qt之函式重定義QT函式
- 介面、虛擬函式、純虛擬函式、抽象類函式抽象
- [Lang] 虛擬函式函式
- 虛擬函式的呼叫原理函式
- 【C++筆記】虛擬函式(從虛擬函式表來解析)C++筆記函式
- 【C++筆記】虛擬函式(從虛擬函式概念來解析)C++筆記函式
- 虛擬函式的實現原理函式
- 建立派生類物件,建構函式的執行順序物件函式
- c++虛擬函式表C++函式
- 虛擬函式與多型函式多型
- 內聯(inline)函式與虛擬函式(virtual)的討論inline函式
- 02_函式定義及使用函式函式
- 如何在函式內部定義函式?函式
- python---函式定義Python函式
- python如何定義函式Python函式
- 虛擬化實戰:對(類)虛擬機器進行實時熱遷移虛擬機
- C++ 介面(純虛擬函式)C++函式
- C++ 虛擬函式表解析C++函式
- 深入C++成員函式及虛擬函式表C++函式
- 兄弟連go教程(11)函式 - 函式定義Go函式
- 什麼是Python函式?如何定義函式?Python函式
- 對beego的控制器函式進行單測Go函式
- 方法(函式)的定義與引數函式
- Shell中函式的定義和使用函式
- ts函式約束定義函式
- 在jQuery定義自己函式jQuery函式
- 虛擬函式的記憶體佈局(上)函式記憶體
- 關於虛擬函式的一些理解函式
- C++多型之虛擬函式C++多型函式
- 抽象基類和純虛擬函式抽象函式
- 函式引數 引數定義函式型別函式型別
- 第 8 節:函式-函式定義和引數函式
- 對稱、非對稱的加密技術是如何對網站資料進行雙重加密?加密網站
- 模型的列表定義中,使用函式時如何定義引數?模型函式
- c語言函式指標的定義C語言函式指標