虛函數(shù)使用方法很簡單局装,直接在函數(shù)名前面添加關(guān)鍵字virtual聲明即可碳想,如果虛函數(shù)末尾增加=0則表示為純虛函數(shù)宋渔,純虛函數(shù)要求所有派生類都必須重寫該該函數(shù),帶有純虛函數(shù)的類我們也稱為虛基類督暂。
虛函數(shù)的實現(xiàn)揪垄,作為一個老生常談的問題,要想透徹的講明白逻翁,還是需要對底層機制有進一步的理解的福侈。
問題拋出
基類指針為什么能調(diào)用子類的虛函數(shù)?
虛函數(shù)實現(xiàn)的關(guān)鍵原理和虛函數(shù)表指針vptr有莫大關(guān)系卢未,vptr實際上是指向一個虛函數(shù)表(一維數(shù)組)肪凛,該表存儲了每個虛函數(shù)的函數(shù)地址,那么虛函數(shù)是如何借助這個vptr實現(xiàn)運行時對象的多態(tài)性辽社,也就是我們常說的動態(tài)綁定伟墙。
從C++對象內(nèi)存結(jié)構(gòu)說起
閱讀過《深度探索C++對象模型》的同學應(yīng)該比較熟悉下面這個結(jié)構(gòu),每一個派生類對象實際上是由兩個部分組成滴铅,如下面的圖所示
- 父類的部分戳葵,包括成員變量、vptr汉匙、成員函數(shù)等拱烁,都是共享給子類(當然有一定的權(quán)限設(shè)置)
- 子類自身的部分生蚁,子類自己成員變量,成員函數(shù)
所以子類就是個特殊的父類戏自,享有父類所有屬性邦投,是is-a的關(guān)系。所以子類也就能直接強制轉(zhuǎn)換為父類擅笔,我們通常使用dynamic_cast將子類指針轉(zhuǎn)為父類指針志衣,那么這個父類指針的訪問域也就變?yōu)閮?nèi)存模型中的上半部分,無法再訪問子類的任何資源猛们,但是有個例外念脯,那就是虛表指針vptr,為什么呢弯淘?
我們來進一步看看vptr在類繼承過程中到底是怎么變化绿店?
通常函數(shù)地址都是在編譯的時候就確定了,但是虛函數(shù)的調(diào)用地址需要到運行的時候才能確定庐橙,因為你無法確定一個基類的指針到底是執(zhí)行基類對象還是子類對象假勿。
實際上,虛函數(shù)表指針是在對象執(zhí)行構(gòu)造函數(shù)的確定的怕午。對于基類來說废登,執(zhí)行基類構(gòu)造函數(shù)時淹魄,直接把虛函數(shù)表填充為基類的虛函數(shù)地址即可郁惜;對于派生類來說,派生類對象構(gòu)造的時候甲锡,會先執(zhí)行父類的構(gòu)造函數(shù)(把虛函數(shù)表全部填充為基類的虛函數(shù)地址)兆蕉,然后再執(zhí)行子類構(gòu)造函數(shù)(對于子類重寫的虛函數(shù),修改虛函數(shù)表中對于的函數(shù)地址缤沦,將其改為子類的虛函數(shù)地址)虎韵,具體過程如下圖2個步驟所示:
動態(tài)綁定
有了以上基礎(chǔ)后,回到之前的問題缸废,動態(tài)綁定是怎么發(fā)生的包蓝?
現(xiàn)在回答這個問題很簡單了,對于一個指向子類對象的基類指針企量,它的vptr其實在子類構(gòu)造過程被改寫過测萎,所以使用基指針調(diào)用虛函數(shù)的時候,如果子類有重寫届巩,會調(diào)用子類的虛函數(shù)硅瞧,如果沒有重寫,則直接調(diào)用基類的虛函數(shù)恕汇,這樣就實現(xiàn)了運行時對象的多態(tài)性腕唧。
代碼實例
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
virtual void sleep()
{
cout<<"animal sleep"<<endl;
}
virtual void breathe()
{
cout<<"animal breathe"<<endl;
}
string name;
};
class Fish:public Animal
{
public:
virtual void breathe()
{
cout<<"fish bubble"<<endl;
}
int skin;
};
int main()
{
Fish fh;
Fish *pFish = &fh;
Animal* pAnimal = dynamic_cast<Animal*>(pFish);
pAnimal->breathe();//fish bubble 執(zhí)行子類重寫的虛函數(shù)
pAnimal->sleep(); //animal sleep 執(zhí)行基類的虛函數(shù)
pAnimal->name; //name位于基類域,能訪問
// pAnimal->skin; skin位于子類域,無法訪問
}