一. 什么為虛函數
簡而言之,在一個類中,前面帶有virtual聲明的成員函數就叫做虛函數,例如
class Base{ public: int x; int y; public: Base(){ x=1; y=2; } virtual void Print(){ //該函數即為虛函數 printf("%d %d\n",x,y); } };
二.虛函數的間接調用
class Base { public: void Function_1() { printf("Function_1...\n"); } virtual void Function_2() //虛函數 { printf("Function_2...\n"); } };
我們生成一個Base實例,通過對象訪問函數,查看反匯編
Base base; base.Function_1(); 00401090 8D 4D FC lea ecx,[ebp-4] 00401093 E8 9F FF FF FF call @ILT+50(Base::Function_1) (00401037) base.Function_2(); 00401098 8D 4D FC lea ecx,[ebp-4] 0040109B E8 65 FF FF FF call @ILT+0(Base::Function_2) (00401005)
我們可以觀察到,Fn1與Fn2都是通過Call指令進行訪問的,即代表着在編譯期間,編譯器就已經給這兩個函數確定的地址,在CPU內留下硬地址,我們利用Call指令就可訪問並執行函數
接着,我們用一個Base類型的指針來訪問函數,查看反匯編得
Base* pb = &base; pb->Function_1(); 004010A6 8B 4D F8 mov ecx,dword ptr [ebp-8] 004010A9 E8 89 FF FF FF call @ILT+50(Base::Function_1) (00401037) pb->Function_2(); 004010AE 8B 4D F8 mov ecx,dword ptr [ebp-8] //將this指針放入ecx中,即結構體的首地址 004010B1 8B 11 mov edx,dword ptr [ecx] //將結構體首地址的前四個字節取出放入到edx中 004010B3 8B F4 mov esi,esp 004010B5 8B 4D F8 mov ecx,dword ptr [ebp-8] 004010B8 FF 12 call dword ptr [edx] //將那四個字節代表的地址中的前四個字節拿出,當做地址進行call
經過分析匯編代碼可以得到,用指針訪問虛函數時,函數的地址並不確定,而是通過各種轉換而得到真正的函數地址並執行
總結:
通過對象調用時,virtual函數和普通函數都是E8 Call,即直接Call
通過指針調用時,普通函數為直接Call,virtual函數為FF Call,即間接Call
三.深入虛函數調用
class Base { public: int x; int y; virtual void Function_1() { printf("Function_1...\n"); } virtual void Function_2() { printf("Function_2...\n"); } };
int main(){
printf("%d",sizeof(Base));
}
當類中含有虛函數時,我們觀察類的大小
去除兩個局部變量的大小,我們發現虛函數的大小為4,而當我們去掉一個虛函數再測試大小時發現,虛函數大小仍為4.
於是去認真研究反匯編代碼得
pb->Function_1(); 0040D9E3 8B 4D F0 mov ecx,dword ptr [ebp-10h] //將結構體首地址放到ecx中 0040D9E6 8B 11 mov edx,dword ptr [ecx] //將首地址的前四個字節拿出放到edx中 0040D9E8 8B F4 mov esi,esp 0040D9EA 8B 4D F0 mov ecx,dword ptr [ebp-10h] //將結構體首地址放到ecx中 0040D9ED FF 12 call dword ptr [edx] pb->Function_2(); 0040D9F6 8B 45 F0 mov eax,dword ptr [ebp-10h] 0040D9F9 8B 10 mov edx,dword ptr [eax] 0040D9FB 8B F4 mov esi,esp 0040D9FD 8B 4D F0 mov ecx,dword ptr [ebp-10h] 0040DA00 FF 52 04 call dword ptr [edx+4] //首地址前四個字節所代表的的地址里面存的值偏移+4
發現帶有虛函數的類中,首地址的前4個字節代表一個新的屬性,這個地址指向一張表,即虛函數表,里面存儲所有虛函數的地址
虛函數表
存儲虛函數地址的表就叫做虛函數表
打印虛函數表
class Base { public: int x; int y; virtual void Function_1() { printf("Function_1...\n"); } virtual void Function_2() { printf("Function_2...\n"); } virtual void Function_3() { printf("Function_3...\n"); } }; void TestMethod() { //查看 Sub 的虛函數表 Base base; //對象的前四個字節就是虛函數表 printf("base 的虛函數表地址為:%x\n",*(int*)&base); //通過函數指針調用函數,驗證正確性 typedef void(*pFunction)(void); pFunction pFn; for(int i=0;i<3;i++) { int temp = *((int*)(*(int*)&base)+i); pFn = (pFunction)temp; pFn(); } }
總結:
當類中有虛函數時,會多出一個屬性,這個屬性占4個字節
多出的一個屬性是虛函數表的地址
四.深入研究虛函數表
struct Base { public: virtual void Function_1() { printf("Function_1...\n"); } virtual void Function_2() { printf("Function_2...\n"); } virtual void Function_3() { printf("Function_3...\n"); } }; int main(int argc, char* argv[]) { //查看 Base 的虛函數表 Base base; //對象的前四個字節就是虛函數表 printf("Base 的虛函數表地址為:%x\n",*(int*)&base); //虛函數表中第1個函數地址 printf("虛函數表中第1個函數地址:%x\n",*((int*)(*(int*)&base)+0)); //虛函數表中第2個函數地址 printf("虛函數表中第2個函數地址:%x\n",*((int*)(*(int*)&base)+1)); //虛函數表中第3個函數地址 printf("虛函數表中第3個函數地址:%x\n",*((int*)(*(int*)&base)+2)); //通過函數指針調用函數,驗證正確性 typedef void(*pFunction)(void); pFunction pFn; pFn = (pFunction)*((int*)(*(int*)&base)+0); pFn(); pFn = (pFunction)*((int*)(*(int*)&base)+1); pFn(); pFn = (pFunction)*((int*)(*(int*)&base)+2); pFn(); return 0; }
下面來研究幾個類型來加強對虛函數的理解
單繼承無函數覆蓋
struct Base { public: virtual void Function_1() { printf("Base:Function_1...\n"); } virtual void Function_2() { printf("Base:Function_2...\n"); } virtual void Function_3() { printf("Base:Function_3...\n"); } }; struct Sub:Base { public: virtual void Function_4() { printf("Sub:Function_4...\n"); } virtual void Function_5() { printf("Sub:Function_5...\n"); } virtual void Function_6() { printf("Sub:Function_6...\n"); } }; int main(int argc, char* argv[]) { //查看 Sub 的虛函數表 Sub sub; //對象的前四個字節就是虛函數表 printf("Sub 的虛函數表地址為:%x\n",*(int*)&sub); //通過函數指針調用函數,驗證正確性 typedef void(*pFunction)(void); pFunction pFn; for(int i=0;i<6;i++) { pFn = (pFunction)*((int*)(*(int*)&sub)+i); pFn(); } return 0; }
得到有一張虛函數表,且每個虛函數得到執行
單繼承有函數覆蓋
struct Base { public: virtual void Function_1() { printf("Base:Function_1...\n"); } virtual void Function_2() { printf("Base:Function_2...\n"); } virtual void Function_3() { printf("Base:Function_3...\n"); } }; struct Sub:Base { public: virtual void Function_1() { printf("Sub:Function_1...\n"); } virtual void Function_2() { printf("Sub:Function_2...\n"); } virtual void Function_6() { printf("Sub:Function_6...\n"); } }; int main(int argc, char* argv[]) { //查看 Sub 的虛函數表 Sub sub; //對象的前四個字節就是虛函數表 printf("Sub 的虛函數表地址為:%x\n",*(int*)&sub); //通過函數指針調用函數,驗證正確性 typedef void(*pFunction)(void); pFunction pFn; for(int i=0;i<6;i++) { int temp = *((int*)(*(int*)&sub)+i); if(temp == 0) { break; } pFn = (pFunction)temp; pFn(); } printf("%x\n",*((int*)(*(int*)&sub)+6)); return 0; }
通過該輸出我們可以得出一個結論,當有函數重載時,子類虛函數會覆蓋基類虛函數,僅執行子類虛函數
多重繼承有函數覆蓋
struct Base1 { public: virtual void Fn_1() { printf("Base1:Fn_1...\n"); } virtual void Fn_2() { printf("Base1:Fn_2...\n"); } }; struct Base2 { public: virtual void Fn_3() { printf("Base2:Fn_3...\n"); } virtual void Fn_4() { printf("Base2:Fn_4...\n"); } }; struct Sub:Base1,Base2 { public: virtual void Fn_1() { printf("Sub:Fn_1...\n"); } virtual void Fn_3() { printf("Sub:Fn_3...\n"); } virtual void Fn_5() { printf("Sub:Fn_5...\n"); } }; int main(int argc, char* argv[]) { //查看 Sub 的虛函數表 Sub sub; //通過函數指針調用函數,驗證正確性 typedef void(*pFunction)(void); //對象的前四個字節是第一個Base1的虛表 printf("Sub 的虛函數表地址為:%x\n",*(int*)&sub); pFunction pFn; for(int i=0;i<6;i++) { int temp = *((int*)(*(int*)&sub)+i); if(temp == 0) { break; } pFn = (pFunction)temp; pFn(); } //對象的第二個四字節是Base2的虛表 printf("Sub 的虛函數表地址為:%x\n",*(int*)((int)&sub+4)); pFunction pFn1; for(int k=0;k<2;k++) { int temp = *((int*)(*(int*)((int)&sub+4))+k); pFn1 = (pFunction)temp; pFn1(); } return 0; }
得出結論,當一個子類同時繼承兩個父類時,會產生兩張虛函數表,且也是子虛函數會覆蓋基類虛函數。
通過虛函數的使用,我們可以引出一句話:虛函數的本質就是動態綁定,即當函數被真正調用時,才能知道函數的地址。
總結:
只有virtual函數是動態綁定的
動態綁定還有一個名字:多態(多態的基礎是虛函數)