C++中虛函數的原理和虛函數表

一, 什麼是虛函數

簡單地說,那些被virtual關鍵字修飾的成員函數,就是虛函數。虛函數的做用,用專業術語來解釋就是實現多態性(Polymorphism),多態性是將接口與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差別而採用不一樣的策略,虛函數是C++的多態性的主要體現,指向基類的指針在操做它的多態類對象時,會根據不一樣的類對象,調用其相應的函數,這個函數就是虛函數。ios

下面咱們從這段代碼中來進行分析:數組

 


ide

運行結果很簡單函數

I am in class A fun1性能

I am in class A fun2this

I am in class B fun1spa

I am in class B fun2指針

但這是否真正作到了多態性呢?No,多態還有個關鍵之處就是一切用指向基類的指針或引用來操做對象。那如今就把main()處的代碼改一改。code

對象

此次的運行結果

I am in class A fun1

I am in class A fun2

I am in class A fun1

I am in class A fun2

問題來了,ptr明明指向的B的對象,爲何調用的倒是A的函數呢?要解決這個問題,就要用到了虛函數,咱們在修改函數

這時候咱們發現運行結果變了

I am in class A fun1

I am in class A fun2

I am in class B fun1

I am in class A fun2

由於fun1是虛函數,B類繼承A類的fun1默認也是虛函數,簡單總結下,指向基類的指針在操做它的多態類對象時,會根據不一樣的類對象,調用其相應的函數,這個函數就是虛函數。

fun2不是虛函數,因此調用的仍舊是A類的fun2函數

二, 虛函數是如何作到的

虛函數是如何作到因對象的不一樣而調用其相應的函數的呢?如今咱們就來剖析虛函數

因爲這兩個類中有虛函數存在,因此編譯器就會爲他們兩個分別插入一段你不知道的數據,併爲他們分別建立一個表。那段數據叫作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指針。經過上面的分析,如今咱們來實做一段代碼,來描述這個帶有虛函數的類的簡單模型

用VC或Dev-C++編譯運行一下,看看結果是否是輸出3,void (*fun)(A*); 這段定義了一個函數指針名字叫作fun,並且有一個A*類型的參數,這個函數指針待會兒用來保存從vtbl裏取出的函數地址

A* p=new B; new B是向內存(內存分5個區:全局名字空間,自由存儲區,寄存器,代碼空間,棧)自由存儲區申請一個內存單元的地址而後隱式地保存在一個指針中.而後把這個地址附值給A類型的指針P.

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; 釋放由p指向的自由空間;

若是調用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)的長度

 


虛函數表

 

類的虛函數表是一塊連續的內存,每一個內存單元中記錄一個JMP指令的地址

  注意的是,編譯器會爲每一個有虛函數的類建立一個虛函數表,該虛函數表將被該類的全部對象共享。類的每一個虛成員佔據虛函數表中的一行。若是類中有N個虛函數,那麼其虛函數表將有N*4字節的大小。

  虛函數(Virtual Function)是經過一張虛函數表(Virtual Table)來實現的。簡稱爲V-Table。在這個表中,主要是一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,保證其真實反應實際的函數。這樣,在有虛函數的類的實例中這個表被分配在了這個實例的內存中,因此,當用父類的指針來操做一個子類的時候,這張虛函數表就顯得由爲重要了,它就像一個地圖同樣,指明瞭實際所應該調用的函數。

  編譯器應該是保證虛函數表的指針存在於對象實例中最前面的位置(這是爲了保證取到虛函數表的有最高的性能——若是有多層繼承或是多重繼承的狀況下)。 這意味着能夠經過對象實例的地址獲得這張虛函數表,而後就能夠遍歷其中函數指針,並調用相應的函數。

相關文章
相關標籤/搜索