生活随笔
收集整理的這篇文章主要介紹了
C++ 虚函数详解(虚函数表、vfptr)——带虚函数表的内存分布图
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
前言
總所周知,虛函數是實現多態的基礎。
引用或指針的靜態類型與對象本身的動態類型的不同,才是C++支持多態的根本所在。 當使用基類的引用或指針調用一個虛函數成員時,會執行動態綁定。 所有的虛函數都必須有定義,因為編譯器直到運行前也不知道到底要調用哪個版本的虛函數。 只有通過指針或引用調用虛函數才會發生動態綁定,因為只有這種情況,引用或指針的靜態類型與對象本身的動態類型才會不同。
關于另一篇博客
大家在網上搜索關于虛函數的博客應該都會搜到陳皓寫的那篇C++ 虛函數表解析吧,這篇文章確實不錯,畫的圖也比較好理解,對于指針理解比較深刻的人應該不會理解錯誤,但對于新人來說可能還是有點不友好。以下幾點我覺得需要強調:
虛函數表的指針,實質是指針的指針。 虛函數表的內容,實質是一個指針的數組。(同時輔證了上一點) 在圖例中,所以就會兩次指針指向的過程。
還有一點就是在該大神的例子程序的輸出中,給出的中文解釋我認為是錯誤的,看起來是很容易誤導人的。 最開始的例子程序中的: cout << "虛函數表地址:" << (int*)(&b) << endl; cout << "虛函數表 — 第一個函數地址:" << (int*)*(int*)(&b) << endl; 這兩句明顯錯誤,本人在困惑之余便開始了自己的驗證。 而且圖例中也應該有兩次指針指向的過程。
虛函數表(vfptr)
虛函數表的指針存儲在對象實例中最前面的位置。 這意味著我們可以通過對象實例的地址得到這個虛函數表的指針,然后就遍歷虛函數表中的各個函數指針,然后調用相應的函數。 下面開始各個例子程序的實驗!(win10+vs2017)
只有基類
#include "pch.h"
#include <iostream>
using namespace std
;
class Base { public : virtual void f ( ) { cout
<< "Base::f" << endl
; } virtual void g ( ) { cout
<< "Base::g" << endl
; } virtual void h ( ) { cout
<< "Base::h" << endl
; } } ;
int main ( )
{ typedef void ( * Fun
) ( void ) ; Base b
; Fun pFun
= NULL ; Base
* p
= & b
; cout
<< "該對象的地址:" << p
<< endl
; cout
<< "虛函數表的指針也是從這個地址" << ( int * ) ( & b
) << "開始存的" << endl
<< endl
; cout
<< "虛函數表的指針指向的地址10進制:" << * ( int * ) ( & b
) << "即虛函數表的指針存的內容" << endl
; cout
<< "即虛函數表的地址:" << ( int * ) * ( int * ) ( & b
) << endl
<< endl
; pFun
= ( Fun
) * ( int * ) * ( int * ) ( & b
) ; cout
<< "第一個虛函數的地址:" << pFun
<< endl
; pFun ( ) ; Fun gFun
= NULL ; gFun
= ( Fun
) * ( ( int * ) * ( int * ) ( & b
) + 1 ) ; Fun hFun
= NULL ; hFun
= ( Fun
) * ( ( int * ) * ( int * ) ( & b
) + 2 ) ;
}
理解內存里每個字節是有編號,這個編號便是我們說的地址。 指針存的是一個地址,我們只關心指針指向的地址(指針存的地址)和指向對象的類型,而不關心指針這個對象本身的地址。 對指針解引用,實際是從指針指向的地址的那個字節開始,按照指向對象的類型的字節大小n,讀取n個字節出來,來組成這個類型的對象。 打印指針時,會打印出來指針指向的地址,以16進制。
b返回Base類型的對象。 &b返回Base *類型的指針。 (int *)(&b)將Base *類型的指針轉換為int *類型的指針,轉換后指針指向地址沒變,但指向對象的類型變了。 *(int *)(&b)對int *類型的指針解引用,從地址開始的那個字節開始,取出sizeof(int)個字節,賦值給一個int對象(因為指針認為自己指向一個int對象)。 (int *)*(int *)(&b)相當于 (int *)后接一個int值,返回一個int指針,將這個int值作為該指針指向的地址值。 *(int *)*(int *)(&b)對int *類型的指針解引用,返回int值。 (Fun)*(int *)*(int *)(&b),Fun是函數指針,后接一個int值,將這個int值作為該函數指針指向的地址值。 如果以上過程你都正確理解,那么你就能理解這句gFun = (Fun)*((int*)*(int*)(&b) + 1);了。首先(int*)*(int*)(&b)將虛函數表的指針轉換為指向指針數組首元素的指針(即轉換過程中,指針指向地址沒變的),然后((int*)*(int*)(&b) + 1)這里就是數組的指針的正常操作,現在這個指針指向了數組的第二個元素(即第二個虛函數指針),最后就是解引用,然后轉換為Fun函數指針。
如果你還沒有理解某個步驟,建議直接查看以下圖例的大圖,配合debug顯示的局部變量表使用 ,再回頭看整個過程。 上圖解釋了虛函數的實現機制:
在有虛函數的基類對象中,肯定至少有三塊不同的內存存儲區域。 首先是對象內存空間,其開始區域,存了虛函數表的指針。 虛函數表實際是一個指針的數組,這些指針就是虛函數的函數指針。 最后是各個虛函數的存儲區域。
虛函數表的結束標志
在上面例子中還需要講一個細節,在虛函數表最后位置有一個字節用來標志虛函數表的結束。
char * end
= NULL ; end
= ( char * ) ( ( int * ) * ( int * ) ( & b
) + 3 ) ;
加入如上代碼便可以得到結束標志,((int*)*(int*)(&b) + 3)這里指向了虛函數表即指針數組的第四個元素,但實際上數組里只有三個指針,所以這里便剛好指向了結束標志。再通過(char*)轉換指針類型,代表指向的是一個字節。 由于我是第二次運行程序,所以地址有點不一樣。這里end指針存的地址,按照之前的例子應該是0x00305b38再加12。 這里你最好再明確下char型存儲的含義:(即ASCII表中:是int型<---->char型的相互轉換關系)
char end1
= '\0' ; char end2
= 0 ; char zero1
= '0' ; char zero2
= 48 ;
單繼承(無虛函數覆蓋)
在此例中,基類有三個虛函數,派生類也有三個虛函數,但派生類一個虛函數也沒有去重寫。
#include "pch.h"
#include <iostream>
using namespace std
;
class Base {
public : virtual void f ( ) { cout
<< "Base::f" << endl
; } virtual void g ( ) { cout
<< "Base::g" << endl
; } virtual void h ( ) { cout
<< "Base::h" << endl
; } } ;
class Derive : public Base
{
public : virtual void f1 ( ) { cout
<< "Derive::f" << endl
; } virtual void g1 ( ) { cout
<< "Derive::g" << endl
; } virtual void h1 ( ) { cout
<< "Derive::h" << endl
; } } ;
int main ( )
{ typedef void ( * Fun
) ( void ) ; Derive d
; Base
* p
= & d
; Fun fFun
= NULL ; fFun
= ( Fun
) * ( ( int * ) * ( int * ) ( & d
) + 0 ) ; Fun gFun
= NULL ; gFun
= ( Fun
) * ( ( int * ) * ( int * ) ( & d
) + 1 ) ; Fun hFun
= NULL ; hFun
= ( Fun
) * ( ( int * ) * ( int * ) ( & d
) + 2 ) ; Fun f1
= NULL ; f1
= ( Fun
) * ( ( int * ) * ( int * ) ( & d
) + 3 ) ; Fun g1
= NULL ; g1
= ( Fun
) * ( ( int * ) * ( int * ) ( & d
) + 4 ) ; Fun h1
= NULL ; h1
= ( Fun
) * ( ( int * ) * ( int * ) ( & d
) + 5 ) ; char * end
= NULL ; end
= ( char * ) ( ( int * ) * ( int * ) ( & d
) + 6 ) ;
}
雖然虛函數表里只能顯示父類的虛函數,但通過增加數組指針的方法,一樣可以獲得派生類的虛函數指針。就算這里是Derive *p1 = &d;也一樣,只顯示基類的三個虛函數。
虛函數指針按照聲明順序放在虛函數表里面。 基類的虛函數在派生類的虛函數前面。
虛函數表的內存模型如下: 但這里我已經厭倦了給每個虛函數生成一個函數指針,所以可以用以下循環:
int main ( )
{ typedef void ( * Fun
) ( void ) ; Derive d
; int * vTable
= ( int * ) * ( int * ) ( & d
) ; for ( int i
= 0 ; i
< 6 ; ++ i
) { printf ( "function : %d :0X%x->" , i
, vTable
[ i
] ) ; Fun f
= ( Fun
) ( vTable
[ i
] ) ; f ( ) ; }
}
vTable[i]相當于給vTable指針加i,再解引用。其實就是數組的用法啦,所以就少了解引用的一步。 打印出來的是各個虛函數的地址。
單繼承(有虛函數覆蓋)
在此例中,派生類只覆蓋了基類的一個函數:f()。
#include "pch.h"
#include <iostream>
using namespace std
;
class Base {
public : virtual void f ( ) { cout
<< "Base::f" << endl
; } virtual void g ( ) { cout
<< "Base::g" << endl
; } virtual void h ( ) { cout
<< "Base::h" << endl
; } } ;
class Derive : public Base
{
public : virtual void f ( ) { cout
<< "Derive::f" << endl
; } virtual void g1 ( ) { cout
<< "Derive::g" << endl
; } virtual void h1 ( ) { cout
<< "Derive::h" << endl
; } } ;
int main ( )
{ typedef void ( * Fun
) ( void ) ; Derive d
; int * vTable
= ( int * ) * ( int * ) ( & d
) ; for ( int i
= 0 ; i
< 5 ; ++ i
) { printf ( "function : %d :0X%x->" , i
, vTable
[ i
] ) ; Fun f
= ( Fun
) ( vTable
[ i
] ) ; f ( ) ; }
}
可以看出:
由于f虛函數被重寫,原本虛函數表(即指針數組)第一個元素是Base::f()的指針,現在被替換為了Derive::f()的指針 其他虛函數按照之前的順序排列
虛函數表的內存模型如下:
多重繼承(無虛函數覆蓋)
在此例中,有三個基類,一個派生類,且派生類一個虛函數也沒有去重寫。
#include "pch.h"
#include <iostream>
using namespace std
;
class Base1 {
public : virtual void f ( ) { cout
<< "Base1::f" << endl
; } virtual void g ( ) { cout
<< "Base1::g" << endl
; } virtual void h ( ) { cout
<< "Base1::h" << endl
; }
} ;
class Base2 {
public : virtual void f ( ) { cout
<< "Base2::f" << endl
; } virtual void g ( ) { cout
<< "Base2::g" << endl
; } virtual void h ( ) { cout
<< "Base2::h" << endl
; }
} ;
class Base3 {
public : virtual void f ( ) { cout
<< "Base3::f" << endl
; } virtual void g ( ) { cout
<< "Base3::g" << endl
; } virtual void h ( ) { cout
<< "Base3::h" << endl
; }
} ;
class Derive : public Base1
, public Base2
, public Base3
{
public : virtual void f1 ( ) { cout
<< "Derive::f" << endl
; } virtual void g1 ( ) { cout
<< "Derive::g" << endl
; } virtual void h1 ( ) { cout
<< "Derive::h" << endl
; }
} ;
typedef void ( * Fun
) ( void ) ;
void printVfun ( int n
, int * vTable
) { for ( int i
= 0 ; i
< n
; ++ i
) { printf ( "function : %d :0X%x->" , i
, vTable
[ i
] ) ; Fun f
= ( Fun
) ( vTable
[ i
] ) ; f ( ) ; } cout
<< "" << endl
;
}
int main ( )
{ Derive d
; int * vTable1
= ( int * ) * ( int * ) ( & d
) ; printVfun ( 6 , vTable1
) ; int * vTable2
= ( int * ) * ( ( int * ) ( & d
) + 1 ) ; printVfun ( 3 , vTable2
) ; int * vTable3
= ( int * ) * ( ( int * ) ( & d
) + 2 ) ; printVfun ( 3 , vTable3
) ;
}
可以看到:
對于繼承到的每個基類,都有一個對應的虛函數表。 派生類的虛函數的指針,被放進了第一個基類對應的虛函數表里。(按照聲明順序來判斷的)
內存模型如下:
多重繼承(有虛函數覆蓋)
在此例中,有三個基類,一個派生類,且派生類重寫了三個基類的同一個虛函數。
#include "pch.h"
#include <iostream>
using namespace std
;
class Base1 {
public : virtual void f ( ) { cout
<< "Base1::f" << endl
; } virtual void g ( ) { cout
<< "Base1::g" << endl
; } virtual void h ( ) { cout
<< "Base1::h" << endl
; }
} ;
class Base2 {
public : virtual void f ( ) { cout
<< "Base2::f" << endl
; } virtual void g ( ) { cout
<< "Base2::g" << endl
; } virtual void h ( ) { cout
<< "Base2::h" << endl
; }
} ;
class Base3 {
public : virtual void f ( ) { cout
<< "Base3::f" << endl
; } virtual void g ( ) { cout
<< "Base3::g" << endl
; } virtual void h ( ) { cout
<< "Base3::h" << endl
; }
} ;
class Derive : public Base1
, public Base2
, public Base3
{
public : virtual void f ( ) { cout
<< "Derive::f" << endl
; } virtual void g1 ( ) { cout
<< "Derive::g" << endl
; } virtual void h1 ( ) { cout
<< "Derive::h" << endl
; }
} ;
typedef void ( * Fun
) ( void ) ;
void printVfun ( int n
, int * vTable
) { for ( int i
= 0 ; i
< n
; ++ i
) { printf ( "function : %d :0X%x->" , i
, vTable
[ i
] ) ; Fun f
= ( Fun
) ( vTable
[ i
] ) ; f ( ) ; } cout
<< "" << endl
;
}
int main ( )
{ Derive d
; int * vTable1
= ( int * ) * ( int * ) ( & d
) ; printVfun ( 5 , vTable1
) ; int * vTable2
= ( int * ) * ( ( int * ) ( & d
) + 1 ) ; printVfun ( 3 , vTable2
) ; int * vTable3
= ( int * ) * ( ( int * ) ( & d
) + 2 ) ; printVfun ( 3 , vTable3
) ;
}
可以看到:
三個基類的虛函數表的第一項,都被替換為Derive::f的指針 這樣任意基類指針指向派生類對象,都可以調用到Derive::f
對象模型如下:
類與虛函數表與虛函數的對應關系
注意本章中的示意圖都只會關注基類的虛函數指針。或者因為重寫,而導致在虛函數表中基類的虛函數指針被替換的情況。(就像局部變量圖中的一樣)
單繼承(無虛函數覆蓋)
在該例中運行:
Base b1
; Base b2
; Derive d1
; Derive d2
;
每一個類對應到一個虛函數表。 兩個虛函數表里各個指針指向的地址都是相同的。
單繼承(有虛函數覆蓋)
Base b
; Derive d
;
基類的虛函數表的三項還是沒有變化 派生類的虛函數表的第一項被替換了
多重繼承(無虛函數覆蓋)
Base1 b1
; Base2 b2
; Base3 b3
; Derive d
;
多重繼承(有虛函數覆蓋)
派生類的每個虛函數表的第一項都被替換為Derive::f()的指針了,因為它把三個基類的f虛函數都重寫了。
總結
以上是生活随笔 為你收集整理的C++ 虚函数详解(虚函数表、vfptr)——带虚函数表的内存分布图 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。