虛函數聯繫到多態,多態聯繫到繼承。因此本文中都是在繼承層次上作文章。沒了繼承,什麼都沒得談。
下面是對C++的虛函數這玩意兒的理解。
一, 什麼是虛函數(若是不知道虛函數爲什麼物,但有急切的想知道,那你就應該從這裏開始)
簡單地說,那些被virtual關鍵字修飾的成員函數,就是虛函數。虛函數的做用,用專業術語來解釋就是實現多態性(Polymorphism),多態性是將接口與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差別而採用不一樣的策略。下面來看一段簡單的代碼
class A{
public:
void print(){ cout<<」This is A」<<endl;}
};
class B:public A{
public:
void print(){ cout<<」This is B」<<endl;}
};
int main(){ //爲了在之後便於區分,我這段main()代碼叫作main1
A a;
B b;
a.print();
b.print();
}
經過class A和class B的print()這個接口,能夠看出這兩個class因個體的差別而採用了不一樣的策略,輸出的結果也是咱們預料中的,分別是This is A和This is B。但這是否真正作到了多態性呢?No,多態還有個關鍵之處就是一切用指向基類的指針或引用來操做對象。那如今就把main()處的代碼改一改。
int main(){ //main2
A a;
B b;
A* p1=&a;
A* p2=&b;
p1->print();
p2->print();
}
運行一下看看結果,喲呵,驀然回首,結果倒是兩個This is A。問題來了,p2明明指向的是class B的對象但倒是調用的class A的print()函數,這不是咱們所指望的結果,那麼解決這個問題就須要用到虛函數
class A{
public:
virtual void print(){ cout<<」This is A」<<endl;} //如今成了虛函數了
};
class B:public A{
public:
void print(){ cout<<」This is B」<<endl;} //這裏須要在前面加上關鍵字virtual嗎?
};
毫無疑問,class A的成員函數print()已經成了虛函數,那麼class B的print()成了虛函數了嗎?回答是Yes,咱們只需在把基類的成員函數設爲virtual,其派生類的相應的函數也會自動變爲虛函數。因此,class B的print()也成了虛函數。那麼對於在派生類的相應函數前是否須要用virtual關鍵字修飾,那就是你本身的問題了。
如今從新運行main2的代碼,這樣輸出的結果就是This is A和This is B了。
如今來消化一下,我做個簡單的總結,指向基類的指針在操做它的多態類對象時,會根據不一樣的類對象,調用其相應的函數,這個函數就是虛函數。
二, 虛函數是如何作到的(若是你沒有看過《Inside The C++ Object Model》這本書,但又急切想知道,那你就應該從這裏開始)
虛函數是如何作到因對象的不一樣而調用其相應的函數的呢?如今咱們就來剖析虛函數。咱們先定義兩個類
class A{ //虛函數示例代碼
public:
virtual void fun(){cout<<1<<endl;}
virtual void fun2(){cout<<2<<endl;}
};
class B:public A{
public:
void fun(){cout<<3<<endl;}
void fun2(){cout<<4<<endl;}
};
因爲這兩個類中有虛函數存在,因此編譯器就會爲他們兩個分別插入一段你不知道的數據,併爲他們分別建立一個表。那段數據叫作vptr指針,指向那個表。那個表叫作vtbl,每一個類都有本身的vtbl,vtbl的做用就是保存本身類中虛函數的地址,咱們能夠把vtbl形象地當作一個數組,這個數組的每一個元素存放的就是虛函數的地址,請看圖
經過上圖,能夠看到這兩個vtbl分別爲class A和class B服務。如今有了這個模型以後,咱們來分析下面的代碼
A *p=new A;
p->fun();
毫無疑問,調用了A::fun(),可是A::fun()是如何被調用的呢?它像普通函數那樣直接跳轉到函數的代碼處嗎?No,實際上是這樣的,首先是取出vptr的值,這個值就是vtbl的地址,再根據這個值來到vtbl這裏,因爲調用的函數A::fun()是第一個虛函數,因此取出vtbl第一個slot裏的值,這個值就是A::fun()的地址了,最後調用這個函數。如今咱們能夠看出來了,只要vptr不一樣,指向的vtbl就不一樣,而不一樣的vtbl裏裝着對應類的虛函數地址,因此這樣虛函數就能夠完成它的任務。
而對於class A和class B來講,他們的vptr指針存放在何處呢?其實這個指針就放在他們各自的實例對象裏。因爲class A和class B都沒有數據成員,因此他們的實例對象裏就只有一個vptr指針。經過上面的分析,如今咱們來實做一段代碼,來描述這個帶有虛函數的類的簡單模型。
#include<iostream>
using namespace std;
//將上面「虛函數示例代碼」添加在這裏
int main(){
void (*fun)(A*);
A *p=new B;
long lVptrAddr;
memcpy(&lVptrAddr,p,4);
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4);
fun(p);
delete p;
system("pause");
}
用VC或Dev-C++編譯運行一下,看看結果是否是輸出3,若是不是,那麼太陽明天確定是從西邊出來。如今一步一步開始分析
void (*fun)(A*); 這段定義了一個函數指針名字叫作fun,並且有一個A*類型的參數,這個函數指針待會兒用來保存從vtbl裏取出的函數地址
A* p=new B; 這個我不太瞭解,算了,不解釋這個了
long lVptrAddr; 這個long類型的變量待會兒用來保存vptr的值
memcpy(&lVptrAddr,p,4); 前面說了,他們的實例對象裏只有vptr指針,因此咱們就放心大膽地把p所指的4bytes內存裏的東西複製到lVptrAddr中,因此複製出來的4bytes內容就是vptr的值,即vtbl的地址
如今有了vtbl的地址了,那麼咱們如今就取出vtbl第一個slot裏的內容
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4); 取出vtbl第一個slot裏的內容,並存放在函數指針fun裏。須要注意的是lVptrAddr裏面是vtbl的地址,但lVptrAddr不是指針,因此咱們要把它先轉變成指針類型
fun(p); 這裏就調用了剛纔取出的函數地址裏的函數,也就是調用了B::fun()這個函數,也許你發現了爲何會有參數p,其實類成員函數調用時,會有個this指針,這個p就是那個this指針,只是在通常的調用中編譯器自動幫你處理了而已,而在這裏則須要本身處理。
delete p;和system("pause"); 這個我不太瞭解,算了,不解釋這個了
若是調用B::fun2()怎麼辦?那就取出vtbl的第二個slot裏的值就好了
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr+4),4); 爲何是加4呢?由於一個指針的長度是4bytes,因此加4。或者memcpy(&fun,reinterpret_cast<long*>(lVptrAddr)+1,4); 這更符合數組的用法,由於lVptrAddr被轉成了long*型別,因此+1就是日後移sizeof(long)的長度
三, 以一段代碼開始
#include<iostream>
using namespace std;
class A{ //虛函數示例代碼2
public:
virtual void fun(){ cout<<"A::fun"<<endl;}
virtual void fun2(){cout<<"A::fun2"<<endl;}
};
class B:public A{
public:
void fun(){ cout<<"B::fun"<<endl;}
void fun2(){ cout<<"B::fun2"<<endl;}
}; //end//虛函數示例代碼2
int main(){
void (A::*fun)(); //定義一個函數指針
A *p=new B;
fun=&A::fun;
(p->*fun)();
fun = &A::fun2;
(p->*fun)();
delete p;
system("pause");
}
你能估算出輸出結果嗎?若是你估算出的結果是A::fun和A::fun2,呵呵,恭喜恭喜,你中圈套了。其實真正的結果是B::fun和B::fun2,若是你想不通就接着往下看。給個提示,&A::fun和&A::fun2是真正得到了虛函數的地址嗎?
首先咱們回到第二部分,經過段實做代碼,獲得一個「通用」的得到虛函數地址的方法
#include<iostream>
using namespace std;
//將上面「虛函數示例代碼2」添加在這裏
void CallVirtualFun(void* pThis,int index=0){
void (*funptr)(void*);
long lVptrAddr;
memcpy(&lVptrAddr,pThis,4);
memcpy(&funptr,reinterpret_cast<long*>(lVptrAddr)+index,4);
funptr(pThis); //調用
}
int main(){
A* p=new B;
CallVirtualFun(p); //調用虛函數p->fun()
CallVirtualFun(p,1);//調用虛函數p->fun2()
system("pause");
}
如今咱們擁有一個「通用」的CallVirtualFun方法。
這個通用方法和第三部分開始處的代碼有何聯繫呢?聯繫很大。因爲A::fun()和A::fun2()是虛函數,因此&A::fun和&A::fun2得到的不是函數的地址,而是一段間接得到虛函數地址的一段代碼的地址,咱們形象地把這段代碼看做那段CallVirtualFun。編譯器在編譯時,會提供相似於CallVirtualFun這樣的代碼,當你調用虛函數時,其實就是先調用的那段相似CallVirtualFun的代碼,經過這段代碼,得到虛函數地址後,最後調用虛函數,這樣就真正保證了多態性。同時你們都說虛函數的效率低,其緣由就是,在調用虛函數以前,還調用了得到虛函數地址的代碼。
最後的說明:本文的代碼能夠用VC6和Dev-C++4.9.8.0經過編譯,且運行無問題。其餘的編譯器小弟不敢保證。其中,裏面的類比方法只能當作模型,由於不一樣的編譯器的低層實現是不一樣的。例如this指針,Dev-C++的gcc就是經過壓棧,看成參數傳遞,而VC的編譯器則經過取出地址保存在ecx中。因此這些類比方法不能看成具體實現ios