靜態多態與動態多態以及虛函數相關

來源:互聯網
上載者:User

什麼是多態

從字面上理解就是多種形態的意思。而多態一詞最初源自希臘語,其含義便是“多種形式”,意思是是具有多種形式或形態的情形,在C++語言中多態有著更廣泛的含義。在C++ primer一書中把具有繼承關係的多個類型稱為多態類型,因為我們能使用這些類型的“多種形式”而無須在意它們的差異。百度百科上提到在物件導向語言中,介面的多種不同的實現方式即為多態。引用Charlie Calverts對多態的描述——多態性是允許你將父物件設定成為一個或更多的他的子物件相等的技術,賦值之後,父物件就可以根據當前賦值給它的子物件的特性以不同的方式運作。簡單的說,就是一句話:允許將子類類型的指標賦值給父類類型的指標。多態性在Object Pascal和C++中都是通過虛函數實現的。
只從概念描述是無法深刻清晰的理解它的,下面我們就具體分析一下。

1、物件類型

這裡所說的物件類型可以用下面的圖來體現:我們通過代碼舉例說明一下


 1 class Derived1:public Base 2 {}; 3 class Derived2:public Base 4 {}: 5 int main() 6 { 7      Derived1* p1 = new Derived1; 8      Base = p1; 9      Derived2* p2 = new Derived1;10      Base = p2;11      return = p212 }

靜態類型在編譯時間總是已知的,他是變數聲明時的類型或運算式產生的類型;動態類型則是變數或運算式表示的記憶體中的對象的類型。動態類型直到運行時才可知道。

2、靜態多態與動態多態

靜態多態和動態多態的區別其實用下面的圖就可以體現:

靜態多態

靜態多態也稱為靜態繫結或早綁。編譯器在編譯期間完成的, 編譯器根據函數實參的類型(可能會進行隱式類型轉換) , 可推斷出要調用那個函數, 如果有對應的函數就調用該函數, 否則出現編譯錯誤。


 1 int Add(int left,int right) 2 { 3      return left + right; 4 } 5 float Add(float left, float right) 6 { 7      return left + right; 8 } 9 int main()10 {   11     cout<<Add(1,2)<<endl;  //調用int Add()函數12     cout<<Add(1.34f,3.21f)<<endl; //調float Add()函數13     return 0;14 }

這裡我把重載歸為了靜態多態。重載的實現是:編譯器根據函數不同的參數表,對同名函數的名稱做修飾,然後這些同名函數就成了不同的函數(至少對於編譯器來說是這樣的)。函數的調用,在編譯器間就已經確定了,是靜態(記住:是靜態)。也就是說,它們的地址在編譯期就綁定了(早綁定)。正是由於重載的這種性質,也有結論認為:重載只是一種語言特性,與多態無關,與物件導向也無關。基於物件導向來說重載的概念並不屬於“物件導向編程”。但是多態性又是一個比較廣泛的概念。這裡為了便於理解先不去深度分析它們的區別,等多態的其它部分具體分析完畢,我們再來說。

動態多態

動態綁定: 在程式執行期間(非編譯期) 判斷所引 用對象的實際類型, 根據其實際類型調用相應的方法。使用virtual關鍵字修飾類的成員 函數時, 指明該函數為虛函數, 衍生類別需要重新實現, 編譯器將實現動態綁定。


              FunTest1(         cout <<  <<       FunTest2(         cout <<  <<        FunTest3(         cout <<  <<        FunTest4(         cout <<  <<    CDerived :              FunTest1(         cout <<  <<        FunTest2(         cout <<  <<       FunTest3( _iTest1) { cout <<  <<        FunTest4( _iTest1,          cout <<  <<        CBase* pBase =      pBase->FunTest1(     pBase->FunTest2(     pBase->FunTest3(     pBase->FunTest4(        }

當我們使用基類的指標或引用調用基類中定義的一個函數時時,我們並不知道該函數真正的對象是什麼類型,因為他可能是一個基類的對象,也可能是一個衍生類別的對象。如果該函數是虛函數,則直到運行時才會知道到及執行哪個版本,判斷的依據是引用或指標所綁定的對象的真實類型。

解構函式、靜態類型函數、友元函數、內嵌函式與虛函數


        CTest&  =( CTest&       *     fri end voi d FunTestFri end() ;12 }

什麼是虛函數

其實在前面的虛擬繼承中我們已經用到了虛函數這個概念,在那裡我們是為瞭解決菱形普通繼承中訪問二義性的問題,但在多態中,他有更大的作用。百度百科中對虛函數是這麼說的:在某基類中聲明為 virtual 並在一個或多個衍生類別中被重新定義的成員函數,用法格式為:virtual 函數傳回型別 函數名(參數表) {函數體};實現多態性,通過指向衍生類別的基類指標或引用,訪問衍生類別中同名覆蓋成員函數。形象的解釋為“求同存異”,它的作用就是實現多態性。

簡單地說,那些被virtual關鍵字修飾的成員函數,就是虛函數。虛函數的作用,用專業術語來解釋就是實現多態性(Polymorphism),多態性是將介面與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異,而採用不同的策略。


 1 class A 2 { 3     public: 4         virtual void print(){cout<<"This is A"<<endl;} 5 }; 6 class B : public A 7 { 8     public: 9     void print(){cout<<"ThisisB"<<endl;}10 };11 int main()12 {13     A a;14     B b;15     A *p1 = &a;16     A *p2 = &b;17     p1->print();18     p2->print();19     return 0;20 }

毫無疑問,class A的成員函數print()已經成了虛函數,那麼class B的print()成了虛函數了嗎?回答是Yes,我們只需在把基類的成員函數設為virtual,其衍生類別的相應的函數也會自動變為虛函數。所以,class B的print()也成了虛函數。那麼對於在衍生類別的相應函數前是否需要用virtual關鍵字修飾,那就是你自己的問題了(文法上可加可不加,不加的話編譯器會自動加上,但為了閱讀方便和規範性,建議加上)。

運行代碼,輸出的結果是This is A和This is B。

總結:指向基類的指標在操作它的多態類對象時,會根據不同的類對象,調用其相應的函數,這個函數就是虛函數。

解構函式與虛函數

當在解構函式前面加virtual關鍵字時報錯:,我們來分析一下原因。

1、虛函數的執行依賴於虛函數表。而虛函數表在建構函式中進行初始化工作,即初始化vptr,讓他指向正確的虛函數表。而在構造對象期間,虛函數表還沒有被初 始化,將無法進行。

2、構造一個對象的時候,必須知道對象的實際類型,而虛函數行為是在運行期間確定實際類型的。而在構造一個對象時,由於對象還未構造成功。編譯器無法知道對象 的實際類型,是該類本身,還是該類的一個衍生類別,或是更深層次的衍生類別。無法確定。

虛函數的意思就是開啟動態綁定,程式會根據對象的動態類型來選擇要調用的方法。然而在建構函式啟動並執行時候,這個對象的動態類型還不完整,沒有辦法確定它到底是什麼類型,故建構函式不能動態綁定。(動態綁定是根據對象的動態類型而不是函數名,在調用建構函式之前,這個對象根本就不存在,它怎麼動態綁定?)
編譯器在調用基類的建構函式的時候並不知道你要構造的是一個基類的對象還是一個衍生類別的對象。

靜態類型函數與虛函數

當我們在靜態類型函數前加virtual關鍵字時報錯:分析:

1、 static成員不屬於任何類對象或類執行個體,所以即使給此函數加上virutal也是沒有任何意義的。

2、static函數沒有this指標,並且不會進入虛函數表的。當通過指標或者引用調用時根本無法把this指標傳遞給static函數,從而無法體現出多態。靜態成員函數與普通成員函數的差別就在於缺少this指標,沒有這個this指標自然也就無從知道name是哪一個對象的成員了。

友元函數與虛函數

當我們在友元函數前加virtual關鍵字時報錯:

因為C++不支援友元函數的繼承,對於沒有繼承特性的函數沒有虛函數的說法。

內聯成員函數與虛函數

內嵌函式就是為了在代碼中直接展開,減少函數調用花費的代價,虛函數是為了在繼承後對象能夠準確的執行自己的動作,這是不可能統一的。(再說了,inline函數在編譯時間被展開,虛函數在運行時才能動態邦定函數)

賦值運算子的重載與虛函數

當我們把賦值運算子的重載定義為虛函數時編譯可以通過,但是一般不建議這麼做雖然可以將operator=定義為虛函數, 但使用時容易混淆。

1、無法給衍生類別的自有成員賦值;

2、調用虛函數要進行查虛表等一系列操作,效率下降。

解構函式與虛函數

解構函式設為虛函數的作用:在類的繼承中,如果有基類指標指向衍生類別,那麼用基類指標delete時,如果不定義成虛函數,衍生類別中派生的那部分無法析構。


 1 #include <stdafx.h> 2 #include <stdio.h> 3 class A 4 { 5     public: 6     A(); 7     virtual~A(); 8 }; 9     A::A()10     {}11     A::~A()12     {13     printf("Delete class APn");14     }15 class B : public A16 {17     public:18     B();19     ~B();20 };21     B::B()22     { }23     B::~B()24     {25     printf("Delete class BPn");26     }27 int main(int argc, char* argv[])28 {29     A *b=new B;30     delete b;31     return 0;32 }

輸出結果為:Delete class B Delete class A
如果把A的virtual去掉:那就變成了Delete class A也就是說不會刪除衍生類別裡的剩餘部分內容,也即不調用衍生類別的虛函數

解構函式總結:

1. 如果我們定義了一個建構函式,編譯器就不會再為我們產生預設建構函式了。
2. 編譯器產生的解構函式是非虛的,除非是一個子類,其父類有個虛析構,此時的函數虛特性來自父類。
3. 有虛函數的類,幾乎可以確定要有個虛解構函式。
4. 如果一個類不可能是基類就不要申明解構函式為虛函數,虛函數是要耗費空間的。
5. 解構函式的異常退出會導致析構不完全,從而有記憶體泄露。最好是提供一個管理類,在管理類中提供一個方法來析構,調用者再根據這個方法的結果決定下一步的操作。
6. 在建構函式不要調用虛函數。在基類構造的時候,虛函數是非虛,不會走到衍生類別中,既是採用的靜態繫結。顯然的是:當我們構造一個子類的對象時,先調用基類的建構函式,構造子類中基類部分,子類還沒有構造,還沒有初始化,如果在基類的構造中調用虛函數,如果可以的話就是調用一個還沒有被初始化的對象,那是很危險的,所以C++中是不可以在構造父類對象部分的時候調用子類的虛函數實現。但是不是說你不可以那麼寫程式,你這麼寫,編譯器也不會報錯。只是你如果這麼寫的話編譯器不會給你調用子類的實現,而是還是調用基類的實現。
7.在解構函式中也不要調用虛函數。在析構的時候會首先調用子類的解構函式,析構掉對象中的子類部分,然後在調用基類的解構函式析構基類部分,如果在基類的解構函式裡面調用虛函數,會導致其調用已經析構了的子類對象裡面的函數,這是非常危險的。
8. 記得在寫衍生類別的拷貝函數時,調用基類的拷貝函數拷貝基類的部分。

總結:

1、 衍生類別重寫基類的虛函數實現多態, 要求函數名 、 參數列表、 傳回值完全相同。 (協變除外)。

2、 基類中定義了 虛函數, 在衍生類別中該函數始終保持虛函數的特性。

3、 只 有類的非靜態成員 函數才能定義為虛函數, 靜態成員 函數不能定義為虛函數。

4、 如果在類外定義虛函數, 只 能在聲明函數時加virtual關鍵字, 定義時不用加。

5、 建構函式不能定義為虛函數, 雖然可以將operator=定義為虛函數, 但最好不要這麼做, 使用時容易混淆。

6、 不要在建構函式和解構函式中調用虛函數, 在建構函式和解構函式中, 對象是不完整的, 可能會出現未定義的行為。

7、 最好將基類的解構函式聲明為虛函數。 ( 因為衍生類別的解構函式跟基類的解構函式名稱不一樣, 但是構成覆蓋, 這裡編譯器做了特殊處理)

8、 虛表是所有類對象執行個體共用的。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.