前言
一直以來都知道虛函數的經典用法,但是除了本科時剛學c++的時候瞭解過,後來因爲做不同的項目在不同的語言之間跳轉(自覺都是淺嘗輒止),這些基本的東西都忘記了,現在重拾並記錄,權當鞏固基礎了。
經典用法
#include<iostream>
using namespace std;
//幾何體類
class Geometry{
public:
virtual void draw(){
cout<<"Geometry Drawing"<<endl;
}
};
//矩形類
class Rect:public Geometry{
public:
virtual void draw(){
cout<<"Rect Drawing"<<endl;
}
};
//圓類
class Circle:public Geometry{
public:
virtual void draw(){
cout<<"Circle Drawing"<<endl;
}
};
void DrawSomething(Geometry *pG){
pG->draw();
}
int main(){
Geometry *geometrys[3];
//1.init
geometrys[0]=new Rect();
geometrys[1]=new Geometry();
geometrys[2]=new Circle();
//2.draw all
for(int i=0;i<3;i++){
DrawSomething(geometrys[i]);
}
return 0;
}
運行結果:
Rect Drawing
Geometry Drawing
Circle Drawing
用虛函數就可以實現DrawSomething
函數這樣接口簡單的函數,而不用寫DrawRect
、DrawCircle
這樣的函數組。
另外這個例子不好的地方是,幾何體實際上可以作爲一個抽象的概念,應該限制它實例化、此時可以把其中的draw
函數設置爲純虛函數,virtual void draw()=0;
.如此幾何體類爲一個抽象類,禁止實例化。
原理
首先來看一下c++對象模型圖(來自《Inside The C++ Object Model》)
圖中可以看到只有非靜態成員變量和虛表指針是存在對象內的。靜態成員函數、成員函數、靜態變量是獨立存儲的(可以認爲所有的類對象都共有一份)。虛表指針指向的虛表中存儲了所有的虛函數。回到代碼中,當執行這句話的時候pG->draw();
時,編譯器檢測到是需要執行虛函數,就會通過該對象實例的vptr指針找到虛函數的地址,進而調用。所以即使子類的指針被轉化爲父類指針調用,當我們需要使用虛函數時,依然是調用子類的虛函數(轉化的時候,vptr的沒有變)。
現在再來看反面,假設我們去掉了上面代碼所有的virtual 關鍵字。運行結果會是如何?結果是:
Geometry Drawing
Geometry Drawing
Geometry Drawing
從對象模型圖中可以看出一些差別。該變量是什麼類型,就會調用該類型的成員函數(我們的例中,都爲Geometry *類型),沒有一個vptr去做區分。有點類似於全局函數,如果對象或對象指針調用成員函數(非虛),實際上就是調用這個獨立於類實例的函數指針。
總的來說不像虛函數和對象實例緊密相連(通過虛表指針),普通成員函數和類實例是分離的(見模型圖)。但是由此產生的問題是,那成員函數如何去使用成員變量呢,實際上編譯器隱式的給成員函數加上一個參數,使得調用時可以把this指針傳進去,通過this指針就可以調用成員變量了。
補充
關於虛函數的實現原理,參考這篇博客 http://www.cnblogs.com/malecrab/p/5572730.html