如何?C++的多態機制,實現運行多態的機制是在基類的函數前加上virtual關鍵字,在衍生類別中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數。
C++ 多態的實現及原理
C++的多態性用一句話概括就是:在基類的函數前加上virtual關鍵字,在衍生類別中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數。如果物件類型是衍生類別,就調用衍生類別的函數;如果物件類型是基類,就調用基類的函數
1:用virtual關鍵字申明的函數叫做虛函數,虛函數肯定是類的成員函數。
2:存在虛函數的類都有一個一維的虛函數表叫做虛表,類的對象有一個指向虛表開始的虛指標。虛表是和類對應的,虛表指標是和對象對應的。
3:多態性是一個介面多種實現,是物件導向的核心,分為類的多態性和函數的多態性。
4:多態用虛函數來實現,結合動態綁定.
5:純虛函數是虛函數再加上 = 0;
6:抽象類別是指包括至少一個純虛函數的類。
純虛函數:virtual void fun()=0;即抽象類別!必須在子類實現這個函數,即先有名稱,沒有內容,在衍生類別實現內容。
我們先看個例子
#include <iostream> #include <stdlib.h>using namespace std; class Father{public: void Face() { cout << "Father's face" << endl; } void Say() { cout << "Father say hello" << endl; }};class Son:public Father{public: void Say() { cout << "Son say hello" << endl; }};int main(){ Son son; Father *pFather=&son; // 隱式類型轉換 pFather->Say(); return 0;}
輸出的結果為:
Father say hello
我們在main()函數中首先定義了一個Son類的對象son,接著定義了一個指向Father類的指標變數pFather,然後利用該變數調用pFather->Say().估計很多人往往將這種情況和c++的多態性搞混淆,認為son實際上是Son類的對象,應該是調用Son類的Say,輸出”Son say hello”,然而結果卻不是.
從編譯的角度來看:
c++編譯器在編譯的時候,要確定每個對象調用的函數(非虛函數)的地址,這稱為早期繫結,當我們將Son類的對象son的地址賦給pFather時,c++編譯器進行了類型轉換,此時c++編譯器認為變數pFather儲存的就是Father對象的地址,當在main函數中執行pFather->Say(),調用的當然就是Father對象的Say函數
從記憶體角度看
Son類對象的記憶體模型如
我們構造Son類的對象時,首先要調用Father類的建構函式去構造Father類的對象,然後才調用Son類的建構函式完成自身部分的構造,從而拼接出一個完整的Son類對象。當我們將Son類對象轉換為Father類型時,該對象就被認為是原對象整個記憶體模型的上半部分,也就是中“Father的對象所佔記憶體”,那麼當我們利用類型轉換後的對象指標去調用它的方法時,當然也就是調用它所在的記憶體中的方法,因此,輸出“Father Say hello”,也就順理成章了。
正如很多人那麼認為,在上面的代碼中,我們知道pFather實際上指向的是Son類的對象,我們希望輸出的結果是son類的Say方法,那麼想到達到這種結果,就要用到虛函數了。
前面輸出的結果是因為編譯器在編譯的時候,就已經確定了對象調用的函數的地址,要解決這個問題就要使用晚綁定,當編譯器使用晚綁定時候,就會在運行時再去確定對象的類型以及正確的調用函數,而要讓編譯器採用晚綁定,就要在基類中聲明函數時使用virtual關鍵字,這樣的函數我們就稱之為虛函數,一旦某個函數在基類中聲明為virtual,那麼在所有的衍生類別中該函數都是virtual,而不需要再顯式地聲明為virtual。
代碼稍微改動一下,看一下運行結果
#include <iostream> #include <stdlib.h>using namespace std; class Father{public: void Face() { cout << "Father's face" << endl; } virtual void Say() { cout << "Father say hello" << endl; }};class Son:public Father{public: void Say() { cout << "Son say hello" << endl; }};int main(){ Son son; Father *pFather=&son; // 隱式類型轉換 pFather->Say(); return 0;}
運行結果:
Son say hello
我們發現結果是”Son say hello”也就是根據對象的類型調用了正確的函數,那麼當我們將Say()聲明為virtual時,背後發生了什麼。
編譯器在編譯的時候,發現Father類中有虛函數,此時編譯器會為每個包含虛函數的類建立一個虛表(即 vtable),該表是一個一維數組,在這個數組中存放每個虛函數的地址,
那麼如何定位虛表呢?編譯器另外還為每個對象提供了一個虛表指標(即vptr),這個指標指向了對象所屬類的虛表,在程式運行時,根據對象的類型去初始化vptr,從而讓vptr正確的指向了所屬類的虛表,從而在調用虛函數的時候,能夠找到正確的函數,對於第二段代碼程式,由於pFather實際指向的物件類型是Son,因此vptr指向的Son類的vtable,當調用pFather->Son()時,根據虛表中的函數地址找到的就是Son類的Say()函數.
正是由於每個對象調用的虛函數都是通過虛表指標來索引的,也就決定了虛表指標的正確初始化是非常重要的,換句話說,在虛表指標沒有正確初始化之前,我們不能夠去調用虛函數,那麼虛表指標是在什麼時候,或者什麼地方初始化呢?
答案是在建構函式中進行虛表的建立和虛表指標的初始化,在構造子類對象時,要先調用父類的建構函式,此時編譯器只“看到了”父類,並不知道後面是否還有繼承者,它初始化父類對象的虛表指標,該虛表指標指向父類的虛表,當執行子類的建構函式時,子類對象的虛表指標被初始化,指向自身的虛表。
總結(基類有虛函數的):
1:每一個類都有虛表
2:虛表可以繼承,如果子類沒有重寫虛函數,那麼子類虛表中仍然會有該函數的地址,只不過這個地址指向的是基類的虛函數實現,如果基類有3個虛函數,那麼基類的虛表中就有三項(虛函數地址),衍生類別也會虛表,至少有三項,如果重寫了相應的虛函數,那麼虛表中的地址就會改變,指向自身的虛函數實現,如果衍生類別有自己的虛函數,那麼虛表中就會添加該項。
3:衍生類別的虛表中虛地址的排列順序和基類的虛表中虛函數地址排列順序相同。
這就是c++中的多態性,當c++編譯器在編譯的時候,發現Father類的Say()函數是虛函數,這個時候c++就會採用晚綁定技術,也就是編譯時間並不確定具體調用的函數,而是在運行時,依據對象的類型來確認調用的是哪一個函數,這種能力就叫做c++的多態性,我們沒有在Say()函數前加virtual關鍵字時,c++編譯器就確定了哪個函數被調用,這叫做早期繫結。
c++的多態性就是通過晚綁定技術來實現的。
c++的多態性用一句話概括就是:在基類的函數前加上virtual關鍵字,在衍生類別中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數,如果物件類型是衍生類別,就調用衍生類別的函數,如果物件類型是基類,就調用基類的函數。
虛函數是在基類中定義的,目的是不確定它的衍生類別的具體行為,例如:
定義一個基類:class Animal //動物,它的函數為breathe()
再定義一個類class Fish //魚。它的函數也為breathe()
再定義一個類class Sheep //羊,它的函數也為breathe()
將Fish,Sheep定義成Animal的衍生類別,然而Fish與Sheep的breathe不一樣,一個是在水中通過水來呼吸,一個是直接呼吸,所以基類不能確定該如何定義breathe,所以在基類中只定義了一個virtual breathe,它是一個空的虛函數,具體的函數在子類中分別定義,程式一般運行時,找到類,如果它有基類,再找到它的基類,最後啟動並執行是基類中的函數,這時,它在基類中找到的是virtual標識的函數,它就會再回到子類中找同名函數,衍生類別也叫子類,基類也叫父類,這就是虛函數的產生,和類的多態性的體現。
這裡的多態性是指類的多態性。
函數的多態性是指一個函數被定義成多個不同參數的函數。當你調用這個函數時,就會調用不同的同名函數。
一般情況下(不涉及虛函數),當我們用一個指標/引用調用一個函數的時候,被調用的函數是取決於這個指標/引用的類型。
當設計到多態性的時候,採用了虛函數和動態綁定,此時的調用就不會在編譯時間候確定而是在運行時確定。不在單獨考慮指標/引用的類型而是看指標/引用的對象的類型來判斷函數的調用,根據對象中虛指標指向的虛表中的函數的地址來確定調用哪個函數
現在我們看一個體現c++多態性的例子,看看輸出結果:
#include <iostream> #include <stdlib.h>using namespace std; class CA { public: void f() { cout << "CA f()" << endl; } virtual void ff() { cout << "CA ff()" << endl; f(); } }; class CB : public CA { public : virtual void f() { cout << "CB f()" << endl; } void ff() { cout << "CB ff()" << endl; f(); CA::ff(); } }; class CC : public CB { public: virtual void f() { cout << "C f()" << endl; } }; int main() { CB b; CA *ap = &b; CC c; CB &br = c; CB *bp = &c; ap->f(); cout << endl; b.f(); cout << endl; br.f(); cout << endl; bp->f(); cout << endl; ap->ff(); cout << endl; bp->ff(); cout << endl; return 0; }
輸出結果:
CA f()CB f()C f()C f()CB ff()CB f()CA ff()CA f()CB ff()C f()CA ff()CA f()