C++多態在編譯和運行期的差別

多態是什麼?簡單來說,就是某段程序調用了一個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越界了,可是並不影響程序的執行哦。

發佈了30 篇原創文章 · 獲贊 13 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章