http://blog.csdn.net/zyq0335/article/details/7657465
1 什么是多態?
多態性可以簡單的概括為“1個接口,多種方法”,在程序運行的過程中才決定調用的機制
程序實現上是這樣,通過父類指針調用子類的函數,可以讓父類指針有多種形態。
2 實現機制
舉一個例子:
#include <iostream.h>
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;
}
};
void main()
{
fish fh;
animal *pAn=&fh;
pAn->breathe();
}
答案是輸出:animal breathe
結果分析:
1從編譯的角度
C++編譯器在編譯的時候,要確定每個對象調用的函數的地址,這稱為早期綁定(early binding),當我們將fish類的對象fh的地址賦給pAn時,C++編譯器進行了類型轉換,此時C++編譯器認為變量pAn保存的就是animal對象的地址。當在main()函數中執行pAn->breathe()時,調用的當然就是animal對象的breathe函數。
2 內存模型的角度
我們構造fish類的對象時,首先要調用animal類的構造函數去構造animal類的對象,然后才調用fish類的構造函數完成自身部分的構造,從而拼接出一個完整的fish對象。當我們將fish類的對象轉換為animal類型時,該對象就被認為是原對象整個內存模型的上半部分,也就是圖1-1中的“animal的對象所占內存”。那么當我們利用類型轉換后的對象指針去調用它的方法時,當然也就是調用它所在的內存中的方法。因此,輸出animal breathe,也就順理成章了。
為了得到我們想要的結果,就要使用虛函數
前面輸出的結果是因為編譯器在編譯的時候,就已經確定了對象調用的函數的地址,要解決這個問題就要使用遲綁定(late binding)技術。當編譯器使用遲綁定時,就會在運行時再去確定對象的類型以及正確的調用函數。而要讓編譯器采用遲綁定,就要在基類中聲明函數時使用virtual關鍵字(注意,這是必須的,很多學員就是因為沒有使用虛函數而寫出很多錯誤的例子),這樣的函數我們稱為虛函數。一旦某個函數在基類中聲明為virtual,那么在所有的派生類中該函數都是virtual,而不需要再顯式地聲明為virtual。
下面我們將上面一段代碼進行部分修改
virtual void breathe()
{
cout<<"animal breathe"<<endl;
}
運行結果:fish bubble
結果分析
編譯器為每個類的對象提供一個虛表指針,這個指針指向對象所屬類的虛表。在程序運行時,根據對象的類型去初始化vptr,從而讓vptr正確的指向所屬類的虛表,從而在調用虛函數時,就能夠找到正確的函數。
由於pAn實際指向的對象類型是fish,因此vptr指向的fish類的vtable,當調用pAn->breathe()時,根據虛表中的函數地址找到的就是fish類的breathe()函數。
正是由於每個對象調用的虛函數都是通過虛表指針來索引的,也就決定了虛表指針的正確初始化是非常重要的。換句話說,在虛表指針沒有正確初始化之前,我們不能夠去調用虛函數。那么虛表指針在什么時候,或者說在什么地方初始化呢?
答案是在構造函數中進行虛表的創建和虛表指針的初始化。還記得構造函數的調用順序嗎,在構造子類對象時,要先調用父類的構造函數,此時編譯器只“看到了”父類,並不知道后面是否后還有繼承者,它初始化父類對象的虛表指針,該虛表指針指向父類的虛表。當執行子類的構造函數時,子類對象的虛表指針被初始化,指向自身的虛表。
當fish類的fh對象構造完畢后,其內部的虛表指針也就被初始化為指向fish類的虛表。在類型轉換后,調用pAn->breathe(),由於pAn實際指向的是fish類的對象,該對象內部的虛表指針指向的是fish類的虛表,因此最終調用的是fish類的breathe()函數。
為了更加清楚的說明內存分布:下面詳細的介紹內存的分布
1 基類的內存分布情況
請看下面的sample
class A
{
void g(){.....}
};
則sizeof(A)=1;
如果改為如下:
class A
{
public:
virtual void f()
{
......
}
void g(){.....}
}
則sizeof(A)=4! 這是因為在類A中存在virtual function,為了實現多態,每個含有virtual function的類中都隱式包含着一個靜態虛指針vfptr指向該類的靜態虛表vtable, vtable中的表項指向類中的每個virtual function的入口地址
例如 我們declare 一個A類型的object :
A c;
A d;
則編譯后其內存分布如下:
從 vfptr所指向的vtable可以看出,每個virtual function都占有一個entry,例如本例中的f函數。而g函數因為不是virtual類型,故不在vtable的表項之內。說明:vtab屬於類成員靜態pointer,而vfptr屬於對象pointer
2 繼承類的內存分布狀況
假設代碼如下:
public B:public A
{
public :
int f() //override virtual function
{
return 3;
}
};
則
A c;
A d;
B e;
編譯后,其內存分布如下:
從中我們可以看出,B類型的對象e有一個vfptr指向vtable address:0x00400030 ,而A類型的對象c和d共同指向類的vtable address:0x00400050a
3 動態綁定過程的實現
我們說多態是在程序進行動態綁定得以實現的,而不是編譯時就確定對象的調用方法的靜態綁定。
其過程如下:
程序運行到動態綁定時,通過基類的指針所指向的對象類型,通過vfptr找到其所指向的vtable,然后調用其相應的方法,即可實現多態。
例如:
A c;
B e;
A *pc=&e; //設置breakpoint,運行到此處
pc=&c;
此時內存中各指針狀況如下:
可以看出,此時pc指向類B的虛表地址,從而調用對象e的方法。
繼續運行,當運行至pc=&c時候,此時pc的vptr值為0x00420050,即指向類A的vtable地址,從而調用c的方法。
這就是動態綁定!(dynamic binding)或者叫做遲后聯編(lazy compile)。
總結:
對於虛函數調用來說,每一個對象內部都有一個虛表指針,該虛表指針被初始化為本類的虛表。所以在程序中,不管你的對象類型如何轉換,但該對象內部的虛表指針是固定的,所以呢,才能實現動態的對象函數調用,這就是C++多態性實現的原理。
需要注意的幾點總結(基類有虛函數):
1、每一個類都有虛表,單繼承的子類擁有一張虛表,子類對象擁有一個虛表指針;若子類是多重繼承(同時繼承多個基類),則子類維護多張虛函數表(針對不同基類構建不同虛表),該子類的對象也將包含多個虛表指針。
關於多重繼承的內存布局情況參考博客:http://blog.chinaunix.net/uid-16723279-id-3568748.html
2、虛表可以繼承,如果子類沒有重寫虛函數,那么子類虛表中仍然會有該函數的地址,只不過這個地址指向的是基類的虛函數實現。如果基類3個虛函數,那么基類的虛表中就有三項(虛函數地址),派生類也會有虛表,至少有三項,如果重寫了相應的虛函數,那么虛表中的地址就會改變,指向自身的虛函數實現。如果派生類有自己的虛函數,那么虛表中就會添加該項。
3、派生類的虛表中虛函數地址的排列順序和基類的虛表中虛函數地址排列順序相同。
2 一個很好的例子 (this指針是指向子類)
#include <iostream.h>
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;
void main()
{
pbase->fn();
}
我在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中定義就可以了
虛函數和純虛函數的區別
1虛函數中的函數是實現的哪怕是空實現,它的作用是這個函數在子類里面可以被重載,運行時動態綁定實現動態
純虛函數是個接口,是個函數聲明,在基類中不實現,要等到子類中去實現
2 虛函數在子類里可以不重載,但是純虛函數必須在子類里去實現。