C++ 多態的實現及原理

C++的多態性用一句話歸納就是:在基類的函數前加上virtual關鍵字,在派生類中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數。若是對象類型是派生類,就調用派生類的函數;若是對象類型是基類,就調用基類的函數ios

  1:用virtual關鍵字申明的函數叫作虛函數,虛函數確定是類的成員函數。  c++

  2:存在虛函數的類都有一個一維的虛函數表叫作虛表,類的對象有一個指向虛表開始的虛指針。虛表是和類對應的,虛表指針是和對象對應的。  數組

  3:多態性是一個接口多種實現,是面向對象的核心,分爲類的多態性和函數的多態性。  函數

  4:多態用虛函數來實現,結合動態綁定.  spa

  5:純虛函數是虛函數再加上 = 0;  設計

  6:抽象類是指包括至少一個純虛函數的類。3d

純虛函數:virtual void fun()=0;即抽象類!必須在子類實現這個函數,即先有名稱,沒有內容,在派生類實現內容。指針

咱們先看個例子code

 1 #include "stdafx.h"
 2 #include <iostream> 
 3 #include <stdlib.h>
 4 using namespace std; 
 5 
 6 class Father
 7 {
 8 public:
 9     void Face()
10     {
11         cout << "Father's face" << endl;
12     }
13 
14     void Say()
15     {
16         cout << "Father say hello" << endl;
17     }
18 };
19 
20 
21 class Son:public Father
22 {
23 public:     
24     void Say()
25     {
26         cout << "Son say hello" << endl;
27     }
28 };
29 
30 void main()
31 {
32     Son son;
33     Father *pFather=&son; // 隱式類型轉換
34     pFather->Say();
35 }

輸出的結果爲:
對象

咱們在main()函數中首先定義了一個Son類的對象son,接着定義了一個指向Father類的指針變量pFather,而後利用該變量調用pFather->Say().估計不少人每每將這種狀況和c++的多態性搞混淆,認爲son其實是Son類的對象,應該是調用Son類的Say,輸出"Son say hello",然而結果卻不是.

 

  從編譯的角度來看:

    c++編譯器在編譯的時候,要肯定每一個對象調用的函數(非虛函數)的地址,這稱爲早期綁定,當咱們將Son類的對象son的地址賦給pFather時,c++編譯器進行了類型轉換,此時c++編譯器認爲變量pFather保存的就是Father對象的地址,當在main函數中執行pFather->Say(),調用的固然就是Father對象的Say函數

 從內存角度看

    

Son類對象的內存模型如上圖

咱們構造Son類的對象時,首先要調用Father類的構造函數去構造Father類的對象,而後才調用Son類的構造函數完成自身部分的構造,從而拼接出一個完整的Son類對象。當咱們將Son類對象轉換爲Father類型時,該對象就被認爲是原對象整個內存模型的上半部分,也就是上圖中「Father的對象所佔內存」,那麼當咱們利用類型轉換後的對象指針去調用它的方法時,固然也就是調用它所在的內存中的方法,所以,輸出「Father Say hello」,也就瓜熟蒂落了。

  正如不少人那麼認爲,在上面的代碼中,咱們知道pFather實際上指向的是Son類的對象,咱們但願輸出的結果是son類的Say方法,那麼想到達到這種結果,就要用到虛函數了。

  前面輸出的結果是由於編譯器在編譯的時候,就已經肯定了對象調用的函數的地址,要解決這個問題就要使用晚綁定,當編譯器使用晚綁定時候,就會在運行時再去肯定對象的類型以及正確的調用函數,而要讓編譯器採用晚綁定,就要在基類中聲明函數時使用virtual關鍵字,這樣的函數咱們就稱之爲虛函數,一旦某個函數在基類中聲明爲virtual,那麼在全部的派生類中該函數都是virtual,而不須要再顯式地聲明爲virtual。

  代碼稍微改動一下,看一下運行結果

  

 1 #include "stdafx.h"
 2 #include <iostream> 
 3 #include <stdlib.h>
 4 using namespace std; 
 5 
 6 class Father
 7 {
 8 public:
 9     void Face()
10     {
11         cout << "Father's face" << endl;
12     }
13 
14     virtual void Say()
15     {
16         cout << "Father say hello" << endl;
17     }
18 };
19 
20 
21 class Son:public Father
22 {
23 public:     
24     void Say()
25     {
26         cout << "Son say hello" << endl;
27     }
28 };
29 
30 void main()
31 {
32     Son son;
33     Father *pFather=&son; // 隱式類型轉換
34     pFather->Say();
35 }

咱們發現結果是"Son say hello"也就是根據對象的類型調用了正確的函數,那麼當咱們將Say()聲明爲virtual時,背後發生了什麼。

  編譯器在編譯的時候,發現Father類中有虛函數,此時編譯器會爲每一個包含虛函數的類建立一個虛表(即 vtable),該表是一個一維數組,在這個數組中存放每一個虛函數的地址,

  

那麼如何定位虛表呢?編譯器另外還爲每一個對象提供了一個虛表指針(即vptr),這個指針指向了對象所屬類的虛表,在程序運行時,根據對象的類型去初始化vptr,從而讓vptr正確的指向了所屬類的虛表,從而在調用虛函數的時候,可以找到正確的函數,對於第二段代碼程序,因爲pFather實際指向的對象類型是Son,所以vptr指向的Son類的vtable,當調用pFather->Son()時,根據虛表中的函數地址找到的就是Son類的Say()函數.

  正是因爲每一個對象調用的虛函數都是經過虛表指針來索引的,也就決定了虛表指針的正確初始化是很是重要的,換句話說,在虛表指針沒有正確初始化以前,咱們不可以去調用虛函數,那麼虛表指針是在何時,或者什麼地方初始化呢?

  答案是在構造函數中進行虛表的建立和虛表指針的初始化,在構造子類對象時,要先調用父類的構造函數,此時編譯器只「看到了」父類,並不知道後面是否還有繼承者,它初始化父類對象的虛表指針,該虛表指針指向父類的虛表,當執行子類的構造函數時,子類對象的虛表指針被初始化,指向自身的虛表。

  

  總結(基類有虛函數的):

  1:每個類都有虛表

  2:虛表能夠繼承,若是子類沒有重寫虛函數,那麼子類虛表中仍然會有該函數的地址,只不過這個地址指向的是基類的虛函數實現,若是基類有3個虛函數,那麼基類的虛表中就有三項(虛函數地址),派生類也會虛表,至少有三項,若是重寫了相應的虛函數,那麼虛表中的地址就會改變,指向自身的虛函數實現,若是派生類有本身的虛函數,那麼虛表中就會添加該項。

  3:派生類的虛表中虛地址的排列順序和基類的虛表中虛函數地址排列順序相同。

  這就是c++中的多態性,當c++編譯器在編譯的時候,發現Father類的Say()函數是虛函數,這個時候c++就會採用晚綁定技術,也就是編譯時並不肯定具體調用的函數,而是在運行時,依據對象的類型來確認調用的是哪個函數,這種能力就叫作c++的多態性,咱們沒有在Say()函數前加virtual關鍵字時,c++編譯器就肯定了哪一個函數被調用,這叫作早期綁定。

  c++的多態性就是經過晚綁定技術來實現的。

  c++的多態性用一句話歸納就是:在基類的函數前加上virtual關鍵字,在派生類中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數,若是對象類型是派生類,就調用派生類的函數,若是對象類型是基類,就調用基類的函數。

  虛函數是在基類中定義的,目的是不肯定它的派生類的具體行爲,例如:

  定義一個基類:class Animal //動物,它的函數爲breathe()

  再定義一個類class Fish //魚。它的函數也爲breathe()

  再定義一個類class Sheep //羊,它的函數也爲breathe()

將Fish,Sheep定義成Animal的派生類,然而Fish與Sheep的breathe不同,一個是在水中經過水來呼吸,一個是直接呼吸,因此基類不能肯定該如何定義breathe,因此在基類中只定義了一個virtual breathe,它是一個空的虛函數,具體的函數在子類中分別定義,程序通常運行時,找到類,若是它有基類,再找到它的基類,最後運行的是基類中的函數,這時,它在基類中找到的是virtual標識的函數,它就會再回到子類中找同名函數,派生類也叫子類,基類也叫父類,這就是虛函數的產生,和類的多態性的體現。

  這裏的多態性是指類的多態性。

  函數的多態性是指一個函數被定義成多個不一樣參數的函數。當你調用這個函數時,就會調用不一樣的同名函數。

 

通常狀況下(不涉及虛函數),當咱們用一個指針/引用調用一個函數的時候,被調用的函數是取決於這個指針/引用的類型。

當設計到多態性的時候,採用了虛函數和動態綁定,此時的調用就不會在編譯時候肯定而是在運行時肯定。不在單獨考慮指針/引用的類型而是看指針/引用的對象的類型來判斷函數的調用,根據對象中虛指針指向的虛表中的函數的地址來肯定調用哪一個函數

 

如今咱們看一個體現c++多態性的例子,看看輸出結果:

 1 #include "stdafx.h"
 2 #include <iostream> 
 3 #include <stdlib.h>
 4 using namespace std; 
 5 
 6 class CA 
 7 { 
 8 public: 
 9     void f() 
10     { 
11         cout << "CA f()" << endl; 
12     } 
13     virtual void ff() 
14     { 
15         cout << "CA ff()" << endl; 
16         f(); 
17     } 
18 }; 
19 
20 class CB : public CA 
21 { 
22 public : 
23     virtual void f() 
24     { 
25         cout << "CB f()" << endl; 
26     } 
27     void ff() 
28     { 
29         cout << "CB ff()" << endl; 
30         f(); 
31         CA::ff(); 
32     } 
33 }; 
34 class CC : public CB 
35 { 
36 public: 
37     virtual void f() 
38     { 
39         cout << "C f()" << endl; 
40     } 
41 }; 
42 
43 int main() 
44 { 
45     CB b; 
46     CA *ap = &b; 
47     CC c; 
48     CB &br = c; 
49     CB *bp = &c; 
50 
51     ap->f(); 
52     cout << endl;
53 
54     b.f(); 
55     cout << endl;
56 
57     br.f(); 
58     cout << endl;
59 
60     bp->f(); 
61     cout << endl;
62 
63     ap->ff(); 
64     cout << endl;
65 
66     bp->ff(); 
67     cout << endl;
68 
69     return 0; 
70 } 

輸出結果:

相關文章
相關標籤/搜索