文章目錄
- 🫧 前言
- 🫧 查看虛表
- 🫧 單繼承下的虛函數表
- 🫧 多繼承下的虛函數表
🫧 前言
多態是一種基于繼承關系的語法,既然涉及到繼承,而繼承的方式有多種:
- 單繼承
- 多繼承
- 棱形繼承
- 棱形虛擬繼承
不同的繼承方式其虛表的形式也不同;
以下操作均為在CentOS7_x64機器上的操作 |
🫧 查看虛表
已知虛表為一個void (*)()
的函數指針數組,除了以內存的方式查看虛表以外還可以使用函數調用的方式來查看虛表的真實情況;
其思路即為將該指針數組的指針打印并調用;
根據函數調用可以知道哪個指針是哪個函數;
typedef void(*VFPTR)();
void PrintVT( VFPTR vTable[] ,size_t n/*虛函數個數*/){cout<<"ptr: "<< vTable <<endl;for(size_t i = 0;i<n;++i){printf(" 第%u地址:0x%x,->",i,vTable[i]);VFPTR f=vTable[i];f();}cout<<endl;
}
//函數的參數為函數指針數組(虛表)的首地址;
//由于是自定義類型的前4/8個字節(在該平臺下為8個字節)
//應使用對應的方式取到前8個字節;
//通過該首地址向后進行遍歷;
🫧 單繼承下的虛函數表
存在一個單繼承關系:
class A{//基類public:virtual void Func1(){//虛函數cout<<"A::Func1()"<<endl;}virtual void Func2(){//虛函數cout<<"A::Func2()"<<endl;}int _a = 10;
};class B:public A{//派生類public:virtual void Func1(){//虛函數且完成重寫cout<<"B::Func1()"<<endl;}virtual void Func3(){//虛函數cout<<"B::Func3()"<<endl;}int _b = 20;
};void test1(){//分別實例化出兩個對象A aa;B bb;
}
使用GDB打印出實例化出的aa
與bb
的內容;
(gdb) display aa
1: aa = {_vptr.A = 0x400ad8 <vtable for A+16>, _a = 10}
(gdb) display bb
2: bb = {<A> = {_vptr.A = 0x400ab0 <vtable for B+16>, _a = 10}, _b = 20}
由于子類對象和父類對象種都存在一張虛表,所以對應的子類對象的虛函數存儲于子類的虛表當中,父類對象的虛函數存儲于父類的虛表當中;
其中該段所出現的結果中的_vptr.A = 0x400ad8
與_vptr.A = 0x400ab0
即為虛表指針,該地址不是兩個對象的地址,而是該對象地址中首地址所存儲的內容;
可以使用&
將兩個對象的地址取出并使用x/x
進行解析從而驗證;
(gdb) p &aa
$10 = (A *) 0x7fffffffe430 #aa對象的首地址
(gdb) x/x 0x7fffffffe430
0x7fffffffe430: 0x00400ad8 #其首地址所存儲的數據(gdb) p &bb
$11 = (B *) 0x7fffffffe420 #bb對象的首地址
(gdb) x/x 0x7fffffffe420
0x7fffffffe420: 0x00400ab0 #其首地址所存儲的數據
其中上面的首地址所存儲的數據即為一個指針,這個指針即為虛表(虛函數表)指針,也就是虛函數表的首地址位置;
在該示例中基類和派生類中各有兩個虛函數,其中派生類的Func1()
虛函數重寫了基類的Func1()
虛函數,所以在基類和派生類的虛表中都存在該函數,且該函數的地址不同;
-
A類虛表
# A類虛表 (gdb) p aa $12 = {_vptr.A = 0x400ad8 <vtable for A+16>, _a = 10} #---------------------------------- (gdb) x/x 0x400ad8 0x400ad8 <_ZTV1A+16>: 0x00400924 #虛表首地址所存儲的數據(A::Func1()函數的地址) (gdb) x/x 0x00400924 0x400924 <A::Func1()>: 0xe5894855 #將地址解析后得到函數 #---------------------------------- (gdb) x/x 0x400ae0 0x400ae0 <_ZTV1A+24>: 0x00400950 #虛表中第二個位置所存儲的數據(由于是64位機器偏移量為8,A::Func2()函數的地址) (gdb) x/x 0x00400950 0x400950 <A::Func2()>: 0xe5894855 #將地址解析后得到函數 #----------------------------------
-
B類虛表
B類虛表與之不同的是,B類作為派生類,而派生類的虛表可以看成是基類虛表的拷貝,且若發生重寫的話虛表中的那個被重寫的函數將會被重寫的函數進行覆蓋;(gdb) p bb # B類虛表 $14 = {<A> = {_vptr.A = 0x400ab0 <vtable for B+16>, _a = 10}, _b = 20} #---------------------------------- (gdb) x/x 0x400ab0 0x400ab0 <_ZTV1B+16>: 0x0040097c #虛表首地址所存儲的數據(B::Func1()函數的地址[已被重寫所以地址不同]) (gdb) x/x 0x0040097c 0x40097c <B::Func1()>: 0xe5894855 #將地址解析后得到函數 #---------------------------------- (gdb) x/x 0x400ab8 0x400ab8 <_ZTV1B+24>: 0x00400950 #虛表中第二個位置所存儲的數據(由于是64位機器偏移量為8,A::Func2()函數的地址[派生類的虛函數表可以看成是基類函數表的拷貝]) (gdb) x/x 0x00400950 0x400950 <A::Func2()>: 0xe5894855 #將地址解析后得到函數 #---------------------------------- (gdb) x/x 0x400ac0 0x400ac0 <_ZTV1B+32>: 0x004009a8 #虛表中第三個位置所存儲的數據(由于是64位機器偏移量為8,B::Func3()函數的地址[這里存放的是B類中自身的函數]) (gdb) x/x 0x004009a8 0x4009a8 <B::Func3()>: 0xe5894855 #將地址解析后得到函數
使用函數查看:
typedef void(*VFPTR)();void PrintVT( VFPTR vTable[] ,size_t n/*虛函數個數*/){cout<<"ptr: "<< vTable <<endl;for(size_t i = 0;i<n;++i){printf(" 第%u地址:0x%x,->",i,vTable[i]);VFPTR f=vTable[i];f();}cout<<endl;
}void test1(){A aa;B bb;PrintVT(*(VFPTR**)&aa,2);PrintVT(*(VFPTR**)&bb,3);
}
結果為 (重新編譯過所以導致最終結果不同,但結論相同):
ptr: 0x400c60第0地址:0x400a94,->A::Func1()第1地址:0x400ac0,->A::Func2()ptr: 0x400c38第0地址:0x400aec,->B::Func1()第1地址:0x400ac0,->A::Func2()第2地址:0x400b18,->B::Func3()
🫧 多繼承下的虛函數表
多繼承下的虛函數表較于單繼承來說會更加的復雜;
復雜的原因在于多繼承為多個基類繼承給一個派生類,那么假設兩個基類都有同名虛函數,且派生類重寫了這個虛函數應該如何判斷?
class A{public:virtual void Func1(){cout<<"A::Func1()"<<endl;}virtual void Func2(){cout<<"A::Func2()"<<endl;}
};class B{public:virtual void Func1(){cout<<"B::Func1()"<<endl;}virtual void Func2(){cout<<"B::Func2()"<<endl;}
};class C : public A,public B{public:virtual void Func1(){cout<<"C::Func1()"<<endl;}virtual void Func3(){cout<<"C::Func3()"<<endl;}
};void test2(){C cc;
}
存在以上的繼承關系;
使用GDB調試該程序并打印cc
的內容;
p cc
$9 = {<A> = {_vptr.A = 0x400cc0 <vtable for C+16>}, <B> = {_vptr.B = 0x400ce8 <vtable for C+56>}, <No data fields>}
由第一點可以知道,派生類的虛表可以看作是基類虛表的拷貝,那么在該程序中由于存在兩個基類(多繼承),所以應當也有兩個虛表;
那么在這個繼承關系中,派生類自身所增加的虛函數處于哪個虛表?
實際上在多繼承關系中,派生類自身所增加的虛函數都在第一個虛表中,且第一張虛表不僅只存在派生類自身的虛函數,還有一個較為關鍵的數據;
- 第一張虛表
#-------64位機器偏移量為8--------- # C::Func1() 被重寫 (gdb) x/x 0x400cc0 0x400cc0 <_ZTV1C+16>: 0x00400b56 (gdb) x/x 0x00400b56 0x400b56 <C::Func1()>: 0xe5894855 #------------------------------- # A::Func2() (gdb) x/x 0x400cc8 0x400cc8 <_ZTV1C+24>: 0x00400ad2 (gdb) x/x 0x00400ad2 0x400ad2 <A::Func2()>: 0xe5894855 #------------------------------- # C::Func3() 派生類自身 (gdb) x/x 0x400cd0 0x400cd0 <_ZTV1C+32>: 0x00400b88 (gdb) x/x 0x00400b88 0x400b88 <C::Func3()>: 0xe5894855 #------------------------------- (gdb) x/x 0x400cd8 0x400cd8 <_ZTV1C+40>: 0xfffffff8 #關鍵數據 #-------------------------------
從該結果可以觀察到,派生類自身的虛函數位于第一張虛表當中;
且在最后一個位置存在一個0xfffffff8
的數據;
- 第二張虛表
從該虛表中能看到第二張虛表的第一個位置所存儲的數據并不是函數指針;#------------------------------- # 所存數據并不為虛函數 (gdb) x/x 0x400ce8 0x400ce8 <_ZTV1C+56>: 0x00400b81 (gdb) x/x 0x00400b81 0x400b81 <_ZThn8_N1C5Func1Ev>: 0x08ef8348 (gdb) x/x 0x08ef8348 0x8ef8348: Cannot access memory at address 0x8ef8348 #------------------------------- # B類中未重寫的虛函數 (gdb) x/x 0x400cf0 0x400cf0 <_ZTV1C+64>: 0x00400b2a (gdb) x/x 0x00400b2a 0x400b2a <B::Func2()>: 0xe5894855 #------------------------------- # NULL空 (gdb) x/x 0x400cf8 0x400cf8 <_ZTV1B>: 0x00000000 #-------------------------------
在這里就可以提到對應的0xfffffff8
數據;
已知0xffffffff
的值為-1,對應的0xfffffff8
即為-8;
這里的值其實是一個偏移量,這個偏移量:當走到該處時將該處的偏移量-8,即得到該處函數所在的位置;
根據這個點進行驗證;
此時已經知道了位置為0x400ce8
,且該位置所存儲的數據為0x00400b81
;
從這里就已經看出,這里通過了偏移量間接的找到了對應的函數;(gdb) x/x 0x400ce8 0x400ce8 <_ZTV1C+56>: 0x00400b81 (gdb) x/x 0x00400b81-8 0x400b79 <C::Func1()+35>: 0xfffcb2e8
當編譯器在處理這段代碼時,將根據偏移量做出一些處理,使得最終能夠通過該偏移量找到對應的函數;
結論為:若是出現多繼承,其中兩個基類都存在同名的虛函數且在派生類中對該虛函數已經完成了重寫的條件時,其虛表構造為如下圖: