標籤:
問題:定義一個空的類型,裡面沒有任何的成員變數或者成員函數,對這個類型進行 sizeof 運算,結果是?
結果是1,因為空白類型的執行個體不包含任何資訊,按道理 sizeof 計算之後結果是0,但是在聲明任何類型的執行個體的時候,必須在記憶體佔有一定的空間,否則無法使用這些執行個體,至於佔據多少記憶體大小,由編譯器決定。
繼續問:如果在這個類型裡添加一個建構函式和解構函式,那麼結果又是多少?
還是1,因為我們調用建構函式和解構函式,只需要知道函數的地址即可,而這些函數的地址只和類型相關,和類型的執行個體無關,編譯器不會為這兩個函數在執行個體內添加任何額外的資訊。
繼續問:如果把解構函式變為虛函數呢?結果是多少?
c++編譯器發現了類型裡有虛函數,,就會為這個類型產生一個虛函數表,並在該類型的每一個執行個體中添加一個指向虛函數表的指標,在32位機器,指標類型大小是4位元組,結果是4,64位機器中,指標大小是8位元組,結果是8。
物件導向的多態的實現效果
多態:同樣的調用語句有多種不同的表現形態
看下面的代碼例子:
class animal{public: void sleep() { cout<<"animal sleep"<<endl; } void breathe() { cout<<"animal breathe"<<endl; }};class fish:public animal{public: void breathe() { cout<<"fish bubble"<<endl; }};int main(void){ fish fh; animal *pAn=&fh; pAn->breathe(); return 0;}
父類指標指向了子類對象,調用了 breathe 方法,那麼結果是animal breathe,也就是說調用的是父類的breathe方法。 這沒有實現多態性。因為C++編譯器在編譯的時候,要確定每個對象調用的函數的地址,這稱為早期繫結(early binding),當fish類的對象fh的地址賦給父類的pAn指標時,C++編譯器進行了類型轉換,它認為父類的指標變數pAn儲存的就是animal對象的地址。當在main()函數中執行pAn->breathe()時,調用的就是animal對象的breathe函數。
進一步說:
在我們構造fish類的對象時,首先要調用父類:animal類的建構函式去構造animal類的對象,然後才調用fish類的建構函式完成自身部分的構造,從而拼接出一個完整的fish對象。當將fish類的對象轉換為animal類型時,該對象就被認為是原對象整個記憶體模型的上半部分,也就是圖中的“animal的對象所佔記憶體”。
那麼當利用類型轉換後的對象指標去調用它的方法時,當然也就是調用它所在的記憶體中的方法。因此,輸出animal breathe。這不是多態的表現形式。
多態實現的三個條件
必要的前提是必須有繼承關係、然後我們需要父類指標(引用)去調用子類的對象,且關鍵是:子類有對父類的虛函數的重寫。virtual關鍵字,告訴編譯器這個函數要支援多態,我們不要根據指標類型判斷如何調用方法,而是要根據指標所指向的實際物件類型來判斷如何調用。
多態的理論基礎
前面的例子,輸出的結果是因為編譯器在編譯的時候,就已經確定了對象調用的函數的地址,要解決這個問題就要使用遲綁定(late binding)技術。當編譯器使用遲綁定時,就會在運行時再去確定對象的類型以及正確的調用函數。而要讓編譯器採用遲綁定,就要在基類中聲明函數時使用virtual關鍵字,這樣的函數我們稱為虛函數。一旦某個函數在基類中聲明為virtual,那麼在所有的衍生類別中該函數都是virtual,而不需要再顯式地聲明為virtual。
所謂的動態聯編:根據實際的物件類型來判斷重寫函數的調用。
C++中多態的實現原理
當類中聲明虛函數時,編譯器會在類中產生一個虛函數表,虛函數表是一個儲存類成員函數指標的資料結構,虛函數表是由編譯器自動產生與維護的,virtual成員函數會被編譯器放入虛函數表中,存在虛函數時,每個對象中都有一個指向虛函數表的指標(vptr指標)
,編譯器為每個類的對象提供一個虛表指標vptr,這個指標指向對象所屬類的虛函數表。在程式運行時,根據對象的類型去初始化vptr,從而讓vptr正確的指向所屬類的虛表,從而在調用虛函數時,就能夠找到正確的函數。
在上例子中:
fish fh; animal *pAn=&fh; pAn->breathe();
由於父類的指標pAn實際指向的物件類型是子類的對象,因此vptr指向的子類fish 類的vtable,當調用pAn->breathe()時,根據虛表中的函數地址找到的就是fish類的breathe()函數。正是由於每個對象調用的虛函數都是通過虛表指標來索引的,也就決定了虛表指標的正確初始化是非常重要的。換句話說,在虛表指標沒有正確初始化之前,我們不能夠去調用虛函數。
那麼虛表指標在什麼時候,或者說在什麼地方初始化呢?
c++是在建構函式中進行虛表的建立和虛表指標的初始化。
建構函式的調用順序:在構造子類對象時,要先調用父類的建構函式,此時編譯器只“看到了”父類,並不知道後面是否後還有繼承者,它初始化父類對象的虛表指標vptr,該虛表指標指向父類的虛表。當執行子類的建構函式時,子類對象的虛表指標vptr被初始化, 此時 vptr指向自身的虛表。當fish類的fh物件建構完畢後,其內部的虛表指標也就被初始化為指向fish類的虛表。
在類型轉換後,調用pAn->breathe(),由於pAn實際指向的是fish類的對象,該對象內部的虛表指標指向的是fish類的虛表,因此最終調用的是fish類的breathe()函數。
說明:
通過虛函數表指標VPTR調用重寫函數是在程式運行時進行的,因此需要通過定址操作才能確定真正應該調用的函數。而普通成員函數是在編譯時間就確定了調用的函數。在效率上,虛函數的效率要低很多。出於效率考慮,沒有必要將所有成員函數都聲明為虛函數
對象在建立的時,由編譯器對VPTR指標進行初始化,只有當對象的構造完全結束後VPTR的指向才最終確定,到底是父類對象的VPTR指向父類虛函數表還是子類對象的VPTR指向子類虛函數表。
回到開始的問題:
class A{ void g(){.....}};則sizeof(A)=1;如果改為如下:class A{public: virtual void f() { ...... } void g(){.....}}
則 sizeof(A)=4,這是因為在類A中存在virtual function,為了實現多態,每個含有virtual function的類中都隱式包含著一個靜態虛指標vptr指向該類的靜態虛表vtable, vtable中的表項指向類中的每個virtual function的入口地址。
多態是在程式進行動態綁定得以實現的,而不是編譯時間就確定對象的調用方法的靜態繫結。
程式運行到動態綁定時,通過基類的指標所指向的物件類型,通過vptr找到其所指向的vtable,然後調用其相應的方法,即可實現多態。這就是動態綁定(dynamic binding)或者叫做遲後聯編(lazy compile)。
class base;base *pbase;class base{public: base() { pbase=this; } virtual void fn() { cout<<"base"<<endl; }};class derived:public base{ void fn() { cout<<"derived"<<endl; }};derived aa;int main(void){ pbase->fn(); return 0;}
在base類的建構函式中將this指標儲存到pbase全域變數中。在定義全域對象aa,即調用derived aa;時,要調用基類的建構函式,先構造基類的部分,然後是子類的部分,由這兩部分拼接出完整的對象aa。
這個this指標指向的當然也就是aa對象,那麼我們在main()函數中利用pbase調用fn(),因為pbase實際指向的是aa對象,而aa對象內部的虛表指標指向的是自身的虛表,最終調用的當然是derived類中的fn()函數。
在derived類中聲明fn()函數時,忘了加public關鍵字,導致聲明為了private(預設為private),但通過前面我們所講述的虛函數調用機制,也就明白了這個地方並不影響它輸出正確的結果。不知道這算不算C++的一個Bug,因為虛函數的調用是在運行時確定調用哪一個函數,所以編譯器在編譯時間,並不知道pbase指向的是aa對象,所以導致這個奇怪現象的發生。如果直接用aa對象去調用,由於物件類型是確定的(注意aa是物件變數,不是指標變數),編譯器往往會採用早期繫結,在編譯時間確定調用的函數,於是就會發現fn()是私人的,不能直接調用。
如果直接在基類的建構函式中調用虛函數,會怎樣?
在調用基類的建構函式時,編譯器只“看到了”父類,並不知道後面是否後還有繼承者,它只是初始化父類對象的虛表指標,讓該虛表指標指向父類的虛表,所以看到結果當然不正確。只有在子類的建構函式調用完畢後,整個虛表才構建完畢,此時才能真正應用C++的多態性。換句話說,不要在建構函式中去調用虛函數實現多態,當然如果只是想調用本類的函數,也無所謂。
得到一個結論:
建構函式中調用多態函數,不能實現多態。
虛函數和純虛函數比較
虛函數
引入原因:為了方便使用多態特性,我們常常需要在基類中定義虛函數。
純虛函數
引入原因:為了實現多態性,純虛函數有點像java中的介面,自己不去實現過程,讓繼承他的子類去實現。在很多情況下,基類本身產生對象是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身產生對象明顯不合常理。 這時我們就將動物類定義成抽象類別,也就是包含純虛函數的類,純虛函數就是基類只定義了函數體,沒有實現過程:
virtual void Eat() = 0; 直接=0 不要 在cpp中定義就可以了
虛函數和純虛函數的區別
虛函數中的函數是實現的哪怕是空實現,它的作用是這個函數在子類裡面可以被重載,運行時動態綁定實現動態,而純虛函數是個介面,是個函式宣告,在基類中不實現,要等到子類中去實現
虛函數在子類裡可以不重載,但是虛函數必須在子類裡去實現。
總結:
對於虛函數調用來,每一個對象內部都有一個虛表指標,該虛表指標被初始化為本類的虛表。所以在程式中,不管你的物件類型如何轉換,但該對象內部的虛表指標是固定的,所以才能實現動態對象函數調用,這就是C++多態性實現的原理。
如果基類有虛函數:
1、每一個類都有虛表。
2、虛表可以繼承,如果子類沒有重寫虛函數,那麼子類虛表中仍然會有該函數的地址,只不過這個地址指向的是基類的虛函數實現。如果基類3個虛函數,那麼基類的虛表中就有三項(虛函數地址),衍生類別也會有虛表,至少有三項,如果重寫了相應的虛函數,那麼虛表中的地址就會改變,指向自身的虛函數實現。如果衍生類別有自己的虛函數,那麼虛表中就會添加該項。
3、衍生類別的虛表中虛函數地址的排列順序和基類的虛表中虛函數地址排列順序相同。
c++編譯器對多態的實現原理總結