本文轉載自:(做了一些改動)
http://www.cnblogs.com/feixiang927/p/5048564.html
基本概念
多態性是一個接口多種實現,分為類的多態性和函數多態性。
函數的多態性(重載)是指一個函數被定義成多個不同參數的函數,它們一般被存在頭文件中,當你調用這個函數,針對不同的參數,就會調用不同的同名函數。
類的多態性用一句話概括就是:在基類的函數前加上virtual關鍵字,在派生類中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數。如果對象類型是派生類,就調用派生類的函數;如果對象類型是基類,就調用基類的函數。多態實現機制
舉個例子
#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
結果分析:
- 從編譯的角度
C++編譯器在編譯的時候,要確定每個對象調用的函數的地址,這稱為早期綁定(early binding),當我們將fish類的對象fh的地址賦給pAn時,C++編譯器進行了類型轉換,此時C++編譯器認為變量pAn保存的就是animal對象的地址。當在main()函數中執行pAn->breathe()時,調用的當然就是animal對象的breathe函數。 -
從內存模型的角度
我們構造fish類的對象時,首先要調用animal類的構造函數去構造animal類的對象,然后才調用fish類的構造函數完成自身部分的構造,從而拼接出一個完整的fish對象。當我們將fish類的對象轉換為animal類型時,該對象就被認為是原對象整個內存模型的上半部分,也就是圖1-1中的“animal的對象所占內存”。那么當我們利用類型轉換后的對象指針去調用它的方法時,當然也就是調用它所在的內存中的方法。因此,輸出animal breathe,也就順理成章了。
1.png
要想得到我們想要的結果就要使用虛函數
前面輸出的結果是因為編譯器在編譯的時候,就已經確定了對象調用的函數的地址,要解決這個問題就要使用遲綁定(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()函數。
在該文章中詳細講解了繼承中類的內存對象模型:http://www.lxweimin.com/p/31373b52902d
虛函數與純虛函數區別:
- 虛函數和純虛函數可以定義在同一個類(class)中,含有純虛函數的類被稱為抽象類(abstract class),而只含有虛函數的類(class)不能被稱為抽象類(abstract class)。
- 虛函數可以被直接使用,也可以被子類(sub class)重載以后以多態的形式調用,而純虛函數必須在子類(sub class)中實現該函數才可以使用,因為純虛函數在基類(base class)只有聲明而沒有定義。
- 虛函數和純虛函數都可以在子類(sub class)中被重載,以多態的形式被調用。
- 虛函數和純虛函數通常存在于抽象基類(abstract base class -ABC)之中,被繼承的子類重載,目的是提供一個統一的接口。
- 虛函數的定義形式:virtual {method body}
純虛函數的定義形式:virtual { } = 0;
在虛函數和純虛函數的定義中不能有static標識符,原因很簡單,被static修飾的函數在編譯時候要求前期bind,然而虛函數卻是動態綁定(run-time bind),而且被兩者修飾的函數生命周期(life recycle)也不一樣。 - 虛函數必須實現,如果不實現,編譯器將報錯,錯誤提示為:
error LNK****: unresolved external symbol "public: virtual void __thiscall
ClassName::virtualFunctionName(void)" - 對于虛函數來說,父類和子類都有各自的版本。由多態方式調用的時候動態綁定。
- 實現了純虛函數的子類,該純虛函數在子類中就變成了虛函數,子類的子類即孫子類可以覆蓋該虛函數,由多態方式調用的時候動態綁定。
- 虛函數是C++中用于實現多態(polymorphism)的機制。核心理念就是通過基類訪問派生類定義的函數。
- 多態性指相同對象收到不同消息或不同對象收到相同消息時產生不同的實現動作。C++支持兩種多態性:編譯時多態性,運行時多態性。
a.編譯時多態性:通過重載函數實現
b 運行時多態性:通過虛函數實現 - 如果一個類中含有純虛函數,那么任何試圖對該類進行實例化的語句都將導致錯誤的產生,因為抽象基類(ABC)是不能被直接調用的。必須被子類繼承重載以后,根據要求調用其子類的方法。