轉載自http://blog.csdn.net/tujiaw/article/details/6753498ios
1. 用virtual關鍵字申明的函數叫作虛函數,虛函數確定是類的成員函數。
2. 存在虛函數的類都有一個一維的虛函數表叫作虛表。類的對象有一個指向虛表開始的虛指針。虛表是和類對應的,虛表指針是和對象對應的。
3. 多態性是一個接口多種實現,是面向對象的核心。分爲類的多態性和函數的多態性。
4. 多態用虛函數來實現,結合動態綁定。
5. 純虛函數是虛函數再加上= 0。
6. 抽象類是指包括至少一個純虛函數的類。
純虛函數:virtual void breathe()=0;即抽象類!必須在子類實現這個函數!即先有名稱,沒內容,在派生類實現內容!數組
咱們先看一個例子:函數
1 #include <iostream.h> 2 class animal 3 { 4 public: 5 void sleep() 6 { 7 cout<<"animal sleep"<<endl; 8 } 9 void breathe() 10 { 11 cout<<"animal breathe"<<endl; 12 } 13 }; 14 class fish:public animal 15 { 16 public: 17 void breathe() 18 { 19 cout<<"fish bubble"<<endl; 20 } 21 }; 22 void main() 23 { 24 fish fh; 25 animal *pAn=&fh; // 隱式類型轉換 26 pAn->breathe(); 27 }
注意,在例1-1的程序中沒有定義虛函數。考慮一下例1-1的程序執行的結果是什麼?
答案是輸出:animal breathe
咱們在main()函數中首先定義了一個fish類的對象fh,接着定義了一個指向animal類的指針變量pAn,將fh的地址賦給了指針變量pAn,而後利用該變量調用pAn->breathe()。許多學員每每將這種狀況和C++的多態性搞混淆,認爲fh其實是fish類的對象,應該是調用fish類的breathe(),輸出「fish bubble」,而後結果卻不是這樣。下面咱們從兩個方面來說述緣由。
一、 編譯的角度
C++編譯器在編譯的時候,要肯定每一個對象調用的函數(要求此函數是非虛函數)的地址,這稱爲早期綁定(early binding),當咱們將fish類的對象fh的地址賦給pAn時,C++編譯器進行了類型轉換,此時C++編譯器認爲變量pAn保存的就是animal對象的地址。當在main()函數中執行pAn->breathe()時,調用的固然就是animal對象的breathe函數。
二、 內存模型的角度
咱們給出了fish對象內存模型,以下圖所示:spa
咱們構造fish類的對象時,首先要調用animal類的構造函數去構造animal類的對象,而後才調用fish類的構造函數完成自身部分的構造,從而拼接出一個完整的fish對象。當咱們將fish類的對象轉換爲animal類型時,該對象就被認爲是原對象整個內存模型的上半部分,也就是圖1-1中的「animal的對象所佔內存」。那麼當咱們利用類型轉換後的對象指針去調用它的方法時,固然也就是調用它所在的內存中的方法。所以,輸出animal breathe,也就瓜熟蒂落了。
正如不少學員所想,在例1-1的程序中,咱們知道pAn實際指向的是fish類的對象,咱們但願輸出的結果是魚的呼吸方法,即調用fish類的breathe方法。這個時候,就該輪到虛函數登場了。
前面輸出的結果是由於編譯器在編譯的時候,就已經肯定了對象調用的函數的地址,要解決這個問題就要使用遲綁定(late binding)技術。當編譯器使用遲綁定時,就會在運行時再去肯定對象的類型以及正確的調用函數。而要讓編譯器採用遲綁定,就要在基類中聲明函數時使用virtual關鍵字(注意,這是必須的,不少學員就是由於沒有使用虛函數而寫出不少錯誤的例子),這樣的函數咱們稱爲虛函數。一旦某個函數在基類中聲明爲virtual,那麼在全部的派生類中該函數都是virtual,而不須要再顯式地聲明爲virtual。
下面修改例1-1的代碼,將animal類中的breathe()函數聲明爲virtual,以下:.net
1 #include <iostream.h> 2 class animal 3 { 4 public: 5 void sleep() 6 { 7 cout<<"animal sleep"<<endl; 8 } 9 virtual void breathe() 10 { 11 cout<<"animal breathe"<<endl; 12 } 13 }; 14 15 class fish:public animal 16 { 17 public: 18 void breathe() 19 { 20 cout<<"fish bubble"<<endl; 21 } 22 }; 23 void main() 24 { 25 fish fh; 26 animal *pAn=&fh; // 隱式類型轉換 27 pAn->breathe(); 28 }
你們能夠再次運行這個程序,你會發現結果是「fish bubble」,也就是根據對象的類型調用了正確的函數。
那麼當咱們將breathe()聲明爲virtual時,在背後發生了什麼呢?
編譯器在編譯的時候,發現animal類中有虛函數,此時編譯器會爲每一個包含虛函數的類建立一個虛表(即vtable),該表是一個一維數組,在這個數組中存放每一個虛函數的地址。對於例1-2的程序,animal和fish類都包含了一個虛函數breathe(),所以編譯器會爲這兩個類都創建一個虛表,(即便子類裏面沒有virtual函數,可是其父類裏面有,因此子類中也有了)以下圖所示:設計
那麼如何定位虛表呢?編譯器另外還爲每一個類的對象提供了一個虛表指針(即vptr),這個指針指向了對象所屬類的虛表。在程序運行時,根據對象的類型去初始化vptr,從而讓vptr正確的指向所屬類的虛表,從而在調用虛函數時,就可以找到正確的函數。對於例1-2的程序,因爲pAn實際指向的對象類型是fish,所以vptr指向的fish類的vtable,當調用pAn->breathe()時,根據虛表中的函數地址找到的就是fish類的breathe()函數。
正是因爲每一個對象調用的虛函數都是經過虛表指針來索引的,也就決定了虛表指針的正確初始化是很是重要的。換句話說,在虛表指針沒有正確初始化以前,咱們不可以去調用虛函數。那麼虛表指針在何時,或者說在什麼地方初始化呢?
答案是在構造函數中進行虛表的建立和虛表指針的初始化。還記得構造函數的調用順序嗎,在構造子類對象時,要先調用父類的構造函數,此時編譯器只「看到了」父類,並不知道後面是否後還有繼承者,它初始化父類對象的虛表指針,該虛表指針指向父類的虛表。當執行子類的構造函數時,子類對象的虛表指針被初始化,指向自身的虛表。對於例2-2的程序來講,當fish類的fh對象構造完畢後,其內部的虛表指針也就被初始化爲指向fish類的虛表。在類型轉換後,調用pAn->breathe(),因爲pAn實際指向的是fish類的對象,該對象內部的虛表指針指向的是fish類的虛表,所以最終調用的是fish類的breathe()函數。
要注意:對於虛函數調用來講,每個對象內部都有一個虛表指針,該虛表指針被初始化爲本類的虛表。因此在程序中,無論你的對象類型如何轉換,但該對象內部的虛表指針是固定的,因此呢,才能實現動態的對象函數調用,這就是C++多態性實現的原理。3d
總結(基類有虛函數):
1. 每個類都有虛表。
2. 虛表能夠繼承,若是子類沒有重寫虛函數,那麼子類虛表中仍然會有該函數的地址,只不過這個地址指向的是基類的虛函數實現。若是基類有3個虛函數,那麼基類的虛表中就有三項(虛函數地址),派生類也會有虛表,至少有三項,若是重寫了相應的虛函數,那麼虛表中的地址就會改變,指向自身的虛函數實現。若是派生類有本身的虛函數,那麼虛表中就會添加該項。
3. 派生類的虛表中虛函數地址的排列順序和基類的虛表中虛函數地址排列順序相同。指針
這就是C++中的多態性。當C++編譯器在編譯的時候,發現animal類的breathe()函數是虛函數,這個時候C++就會採用遲綁定(late binding)技術。也就是編譯時並不肯定具體調用的函數,而是在運行時,依據對象的類型(在程序中,咱們傳遞的fish類對象的地址)來確認調用的是哪個函數,這種能力就叫作C++的多態性。咱們沒有在breathe()函數前加virtual關鍵字時,C++編譯器在編譯時就肯定了哪一個函數被調用,這叫作早期綁定(early binding)。
C++的多態性是經過遲綁定技術來實現的。
C++的多態性用一句話歸納就是:在基類的函數前加上virtual關鍵字,在派生類中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數。若是對象類型是派生類,就調用派生類的函數;若是對象類型是基類,就調用基類的函數。
虛函數是在基類中定義的,目的是不肯定它的派生類的具體行爲。例:
定義一個基類:class Animal//動物。它的函數爲breathe()//呼吸。
再定義一個類class Fish//魚 。它的函數也爲breathe()
再定義一個類class Sheep //羊。它的函數也爲breathe()
爲了簡化代碼,將Fish,Sheep定義成基類Animal的派生類。
然而Fish與Sheep的breathe不同,一個是在水中經過水來呼吸,一個是直接呼吸空氣。因此基類不能肯定該如何定義breathe,因此在基類中只定義了一個virtual breathe,它是一個空的虛函數。具本的函數在子類中分別定義。程序通常運行時,找到類,若是它有基類,再找它的基類,最後運行的是基類中的函數,這時,它在基類中找到的是virtual標識的函數,它就會再回到子類中找同名函數。派生類也叫子類。基類也叫父類。這就是虛函數的產生,和類的多態性(breathe)的體現。
這裏的多態性是指類的多態性。
函數的多態性是指一個函數被定義成多個不一樣參數的函數,它們通常被存在頭文件中,當你調用這個函數,針對不一樣的參數,就會調用不一樣的同名函數。例:Rect()//矩形。它的參數能夠是兩個座標點(point,point)也多是四個座標(x1,y1,x2,y2)這叫函數的多態性與函數的重載。
類的多態性,是指用虛函數和延遲綁定來實現的。函數的多態性是函數的重載。
通常狀況下(沒有涉及virtual函數),當咱們用一個指針/引用調用一個函數的時候,被調用的函數是取決於這個指針/引用的類型。即若是這個指針/引用是基類對象的指針/引用就調用基類的方法;若是指針/引用是派生類對象的指針/引用就調用派生類的方法,固然若是派生類中沒有此方法,就會向上到基類裏面去尋找相應的方法。這些調用在編譯階段就肯定了。code
當設計到多態性的時候,採用了虛函數和動態綁定,此時的調用就不會在編譯時候肯定而是在運行時肯定。不在單獨考慮指針/引用的類型而是看指針/引用的對象的類型來判斷函數的調用,根據對象中虛指針指向的虛表中的函數的地址來肯定調用哪一個函數。對象