多態是什麼?簡單來說,就是某段程序調用了一個API接口,但是這個API有許多種實現,根據上下文的不同,調用這段API的程序,會調用該API的不同實現。今天我們只關注繼承關係下的多態。
還是得通過一個例子來看看C++是怎樣在編譯期和運行期來實現多態的。很簡單,定義了一個Father類,它有一個testVFunc虛函數喲。再定義了一個繼承Father的Child類,它重新實現了testVFunc函數,當然,它也學習Father定義了普通的成員函數testFunc。大家猜猜程序的輸出是什麼?
#include <iostream> using namespace std; class Father { public: int m_fMember; void testFunc(){ cout<<"Father testFunc "<<m_fMember<<endl; } virtual void testVFunc(){ cout<<"Father testVFunc "<<m_fMember<<endl; } Father(){m_fMember=1;} }; class Child : public Father{ public: int m_cMember; Child(){m_cMember=2;} virtual void testVFunc(){cout<<"Child testVFunc "<<m_cMember<<":"<<m_fMember<<endl;} void testFunc(){cout<<"Child testFunc "<<m_cMember<<":"<<m_fMember<<endl;} void testNFunc(){cout<<"Child testNFunc "<<m_cMember<<":"<<m_fMember<<endl;} }; int main() { Father* pRealFather = new Father(); Child* pFalseChild = (Child*)pRealFather; Father* pFalseFather = new Child(); pFalseFather->testFunc(); pFalseFather->testVFunc(); pFalseChild->testFunc(); pFalseChild->testVFunc(); pFalseChild->testNFunc(); return 0; }
同樣調用了testFunc和testVfunc,輸出截然不同,這就是多態了。它的g++編譯器輸出結果是:
Father testFunc 1 Child testVFunc 2:1 Child testFunc 0:1 Father testVFunc 1 Child testNFunc 0:1
看看main函數裏調用的五個test*Func方法吧,這裏有靜態的多態,也有動態的多態。編譯是靜態的,運行是動態的。以下解釋C++編譯器是怎麼形成上述結果的。
首先讓我們用gcc -S來生成彙編代碼,看看main函數裏是怎麼調用這五個test*Func方法的。
movl $16, %edi call _Znwm movq %rax, %rbx movq %rbx, %rdi call _ZN6FatherC1Ev movq %rbx, -32(%rbp) movq -32(%rbp), %rax movq %rax, -24(%rbp) movl $16, %edi call _Znwm movq %rax, %rbx movq %rbx, %rdi call _ZN5ChildC1Ev movq %rbx, -16(%rbp) movq -16(%rbp), %rdi call _ZN6Father8testFuncEv 本行對應pFalseFather->testFunc(); movq -16(%rbp), %rax movq (%rax), %rax movq (%rax), %rax movq -16(%rbp), %rdi call *%rax 本行對應pFalseFather->testVFunc(); movq -24(%rbp), %rdi call _ZN5Child8testFuncEv 本行對應pFalseChild->testFunc(); movq -24(%rbp), %rax movq (%rax), %rax movq (%rax), %rax movq -24(%rbp), %rdi call *%rax 本行對應pFalseChild->testVFunc(); movq -24(%rbp), %rdi call _ZN5Child9testNFuncEv 本行對應pFalseChild->testNFunc(); movl $0, %eax addq $40, %rsp popq %rbx leave
紅色的代碼,就是在依次調用上面5個test*Func。可以看到,第1、3次testFunc調用,其結果已經在編譯出來的彙編語言中定死了,C++代碼都是調用某個對象指針指向的testFunc()函數,輸出結果卻不同,第1次是:Father testFunc 1,第3次是:Child testFunc 0:1,原因何在?在編譯出的彙編語言很明顯,第一次調用的是_ZN6Father8testFuncEv代碼段,第三次調用的是_ZN5Child8testFuncEv代碼段,兩個不同的代碼段!編譯完就已經決定出同一個API用哪種實現,這就是編譯期的多態。
第2、4次testVFunc調用則不然,編譯完以後也不知道以後究竟是調用Father還是Child的testVFunc實現,直到運行時,拿到CPU寄存器裏的指針了,才知道這個指針究竟指向Father還是Child的testVFunc實現。這就是運行期的多態了。
現在我們看看,C++的對象模型是怎麼實現這一點的,以及爲什麼最後打印的是如此結果。還以上面的代碼做例子,生成的pFalseFather指向的對象是一個Child對象,它的內存佈局是:
再來看看調用代碼:
Father* pFalseFather = new Child(); pFalseFather->testFunc(); pFalseFather->testVFunc();
當我們調用pFaseFather->testFunc()代碼時,這不是個virtual函數,所以,彙編代碼裏直接調用了Father::testFunc()實現,這是C++的規則。C++中,如果不是virtual字段的成員函數,調用它的程序將在編譯時就直接調用到函數實現。所以,這行代碼將執行以下C++代碼:
void testFunc(){ cout<<"Father testFunc "<<m_fMember<<endl; }
注意到,pFaseFather指向的是個Child對象,所以Child對象在生成時同時執行了自己和Father父類的構造函數,所以,m_fMember被初始化爲1,打印的結果就是Father testFunc 1。
而pFalseFather->testVFunc();調用了vptl指向的函數,上面說了,pFaseFather指向的是個Child對象,而Child對象實現了自己的testVFunc方法,在你new一個Child對象時,編譯器會將vptl指向它自己的testVFunc的。所以,將會執行下面的C++代碼:
virtual void testVFunc(){cout<<"Child testVFunc "<<m_cMember<<":"<<m_fMember<<endl;}
m_cMemeber被Child的構造函數初始化爲2,m_fMember被Father的構造函數初始化爲1,所以打印出的結果是:Child testVFunc 2:1。
下面我們看看最後三個調用:
pFalseChild->testFunc(); pFalseChild->testVFunc(); pFalseChild->testNFunc();
我們生成了一個pRealFather指向Father對象,它的內存空間是這樣的:
而後我們通過:
Child* pFalseChild = (Child*)pRealFather;
指針pFalseChild是個Child類型,但它實際指向的是個Father對象。首先它調用testFunc函數,到底執行Father還是Child的實現呢?上面說過,非virtual函數一律編譯期根據類型決定,所以,它調用的是Child實現:
void testFunc(){cout<<"Child testFunc "<<m_cMember<<":"<<m_fMember<<endl;}
這裏,m_fMember被Father的構造函數初始化爲1,而m_cMember已經內存越界了!沒錯,在32位機器上,Father對象只有8個字節,而Child對象有12個字節,訪問的m_cMember就是第9-12個字節轉換成的int類型。通常情況,這段內存都是全0的,所以,m_cMember是0。看看結果:Child testFunc 0:1。
然後它調用testVFunc了,這次執行父類還是子類的?是父類的,因爲這個對象是Father對象,在new出來的時候,Father的構造函數會把vptl指針指向自己的testVFunc實現喲。所以將會執行C++代碼:
virtual void testVFunc(){ cout<<"Father testVFunc "<<m_fMember<<endl; }
執行結果自然是:Father testVFunc 1。
最後一個調用testNFunc,真實的Father對象對應的Father類中可沒有這個函數,但是實際編譯執行都沒問題,why?同上理,在main函數中,因爲指針pFalseChild是個Child類型,編譯完的彙編語言在pFalseChild->testNFunc();這裏就直接調用Child的testNFunc實現了,雖然m_cMember越界了,可是並不影響程序的執行哦。