C++虛函數:
- 僅在定義父類成員函數的函數原型前加關鍵字virtual,子類如果重寫了父類的虛函數那么子類前的virtual
關鍵字可寫可不寫,但是為了代碼具有可讀性,最好還是加上virtual關鍵字。 - 子類重寫父類虛函數的條件:
子類的函數名稱與父類的虛函數名稱相同,參數列表也要相同,返回值也相同(如果返回的是子類類型的指針或
者引用也可以),調用約定也相同,即使沒有寫virtual關鍵字,編譯器也視為重寫父類虛函數 - 多態就是一種函數覆蓋:
函數覆蓋:a.作用域不同
b.函數名/參數列表/ 返回值/調用約定相同
c.該函數必須為虛函數
函數重載:a.作用域相同 b.函數名相同 c.參數列表相同(不考慮返回值和調用約定)
數據隱藏:a.作用域不同 b.函數名稱相同
虛表指針:
-
VC++編譯器在編譯時發現如果一個類有虛函數,那么編譯器將會為這個類生成一個虛表(類似函數指針數組),
並且VC++編譯器會在該類的第一個數據成員前插入一個指向該虛表的指針
下面用簡單的代碼測下:class CTest { int m_nTest; public: CTest():m_nTest(1){} void virtual ShowInfo() { std::cout << m_nTest << std::endl; } void virtual ShowInfo1() { std::cout << m_nTest+1 << std::endl; } void virtual ShowInfo2() { std::cout << m_nTest+2 << std::endl; } }; int main(int argc, char* argv[]) { CTest t; t.ShowInfo(); t.ShowInfo1(); t.ShowInfo2(); return 0; }
讓程序停在這個斷點處:
在監視窗口中查看類對象t所在內存地址,並在內存窗口中查看t的內存布局:
可以看出對象t的起始地址為0x0048F730,但是這個地址存放的並不是數據成員m_nTest,其實VS的監視窗口已
經將其解釋為_vfptr(虛表指針),_vfptr指針的值為0x001babdc,現在轉到這個地址處的內存:
這個三個指針分別對應虛函數ShowInfo,ShowInfo1,ShowInfo3,如下圖:
因為這是debug版的程序,所以會有jmp跳轉,方便調試,如果是Release版的程序將不會有這些jmp指令,
調用時直接轉移到對應的函數中 -
虛表中的虛函數順序與虛函數在類中的聲明位置有關,在類中第一個聲明的虛函數在虛表中的位置總是第一個
第二個虛函數則排放在虛表中的第二個位置,依次排放
下面做個測試,調整虛函數在類中的聲明位置,查看其在虛表中的位置變化:
例:虛函數在類中聲明如下:class CTest { int m_nTest; public: CTest():m_nTest(1){} void virtual ShowInfo() { std::cout << m_nTest << std::endl; } void virtual ShowInfo1() { std::cout << m_nTest+1 << std::endl; } void virtual ShowInfo2() { std::cout << m_nTest+2 << std::endl; } };
此時虛函數在虛表中的位置如下:
調整虛函數在類中的聲明位置如下:class CTest { int m_nTest; public: CTest():m_nTest(1){} void virtual ShowInfo1() { std::cout << m_nTest + 1 << std::endl; } void virtual ShowInfo() { std::cout << m_nTest << std::endl; } void virtual ShowInfo2() { std::cout << m_nTest+2 << std::endl; } };
此時虛函數在虛表中的位置如下:
可以看出隨着虛函數在類中聲明位置的變化,虛函數在虛表中的位置也發生對應的改變
直接調用與間接調用(虛調用)
-
通過類對象的方式調用虛函數稱為直接調用,編譯器直接生成調用該虛函數的代碼
例:int main(int argc, char* argv[]) { CTest t; t.ShowInfo(); t.ShowInfo1(); t.ShowInfo2(); return 0; }
觀察上面代碼的反匯編代碼:
CTest t; 008D1A58 lea ecx,[t] 008D1A5B call CTest::CTest (08D1096h) t.ShowInfo(); 008D1A60 lea ecx,[t] 008D1A63 call CTest::ShowInfo (08D10B4h) t.ShowInfo1(); 008D1A68 lea ecx,[t] 008D1A6B call CTest::ShowInfo1 (08D11B3h) t.ShowInfo2(); 008D1A70 lea ecx,[t] 008D1A73 call CTest::ShowInfo2 (08D104Bh) return 0; 008D1A78 xor eax,eax
可以看出VC++編譯器生成的直接調用對應虛函數的代碼
-
通過指向對象的指針或引用調用虛函數,稱為間接調用,編譯器生成直接調用虛函數的代碼,而是通過虛表指針
取出虛表內的虛函數指針,然后用虛函數指針調用虛函數例:
int main(int argc, char* argv[]) { CTest t; CTest & rt = t; CTest * pt = &t; rt.ShowInfo2(); pt->ShowInfo(); return 0; }
觀察上面代碼的反匯編代碼:
CTest t; 00121A58 lea ecx,[t] 00121A5B call CTest::CTest (0121096h) CTest & rt = t; 00121A60 lea eax,[t] 00121A63 mov dword ptr [rt],eax CTest * pt = &t; 00121A66 lea eax,[t] 00121A69 mov dword ptr [pt],eax rt.ShowInfo2(); 00121A6C mov eax,dword ptr [rt] //取對象t的地址 00121A6F mov edx,dword ptr [eax] //取虛表指針 00121A71 mov esi,esp 00121A73 mov ecx,dword ptr [rt] 00121A76 mov eax,dword ptr [edx+8] //取出ShowInfo2在虛表中的函數指針 00121A79 call eax //調用虛函數ShowInfo2 00121A7B cmp esi,esp 00121A7D call __RTC_CheckEsp (0121140h) pt->ShowInfo(); 00121A82 mov eax,dword ptr [pt] //取對象t的地址 00121A85 mov edx,dword ptr [eax] //取虛表指針 00121A87 mov esi,esp 00121A89 mov ecx,dword ptr [pt] 00121A8C mov eax,dword ptr [edx+4] //取出ShowInfo在虛表中的函數指針 00121A8F call eax //調用虛函數ShowInfo 00121A91 cmp esi,esp 00121A93 call __RTC_CheckEsp (0121140h) return 0; 00121A98 xor eax,eax
-
在普通成員函數中調用虛函數依然是間接調用
例:在CTest類中加入一個如下的成員函數:void Test() { ShowInfo(); }
使用如下代碼測試:
int main(int argc, char* argv[]) { CTest t; t.Test(); return 0; }
Test函數對應的反匯編代碼如下:
void Test() { ShowInfo(); } 013719D0 push ebp 013719D1 mov ebp,esp 013719D3 sub esp,0CCh 013719D9 push ebx 013719DA push esi 013719DB push edi 013719DC push ecx 013719DD lea edi,[ebp-0CCh] 013719E3 mov ecx,33h 013719E8 mov eax,0CCCCCCCCh 013719ED rep stos dword ptr es:[edi] 013719EF pop ecx 013719F0 mov dword ptr [this],ecx 013719F3 mov eax,dword ptr [this] 013719F6 mov edx,dword ptr [eax] 013719F8 mov esi,esp 013719FA mov ecx,dword ptr [this] //取虛表指針 013719FD mov eax,dword ptr [edx+4] //從虛表中取虛函數ShowInfo的指針 01371A00 call eax //調用虛函數 01371A02 cmp esi,esp 01371A04 call __RTC_CheckEsp (01371140h) 01371A09 pop edi 01371A0A pop esi 01371A0B pop ebx 01371A0C add esp,0CCh 01371A12 cmp ebp,esp 01371A14 call __RTC_CheckEsp (01371140h) 01371A19 mov esp,ebp 01371A1B pop ebp 01371A1C ret
可以看出在成員函數中調用虛函數是間接調用,也是根據虛表來調用
-
在構造函數和析構函數中不會通過虛表來調用虛函數,而是直接在編譯時生成直接調用虛函數的代碼
例:CTest():m_nTest(1) { ShowInfo1(); } ~CTest() { ShowInfo1(); }
對應反匯編代碼如下:
CTest():m_nTest(1) 000D181C mov eax,dword ptr [this] 000D181F mov dword ptr [eax+4],1 ShowInfo1(); 000D1826 mov ecx,dword ptr [this] 000D1829 call CTest::ShowInfo1 (0D11C2h)
~CTest() { 000D1860 push ebp 000D1861 mov ebp,esp 000D1863 push 0FFFFFFFFh 000D1865 push 0D60D0h 000D186A mov eax,dword ptr fs:[00000000h] 000D1870 push eax 000D1871 sub esp,0CCh 000D1877 push ebx 000D1878 push esi 000D1879 push edi 000D187A push ecx 000D187B lea edi,[ebp-0D8h] 000D1881 mov ecx,33h 000D1886 mov eax,0CCCCCCCCh 000D188B rep stos dword ptr es:[edi] 000D188D pop ecx 000D188E mov eax,dword ptr [__security_cookie (0DB004h)] 000D1893 xor eax,ebp 000D1895 push eax 000D1896 lea eax,[ebp-0Ch] 000D1899 mov dword ptr fs:[00000000h],eax 000D189F mov dword ptr [this],ecx 000D18A2 mov eax,dword ptr [this] 000D18A5 mov dword ptr [eax],offset CTest::`vftable' (0D8B34h) ShowInfo1(); 000D18AB mov ecx,dword ptr [this] 000D18AE call CTest::ShowInfo1 (0D11C2h) //直接調用 }
構造和析構函數是否能為虛函數?
- 構造函數不能為虛函數,因為如果對象都沒有創建,就無法調用虛函數,構造函數為虛函數是沒有任何意義的。
- 析構函數可以是虛函數,在某些情況下必須為虛函數:當一個基類指針指向動態分配的子類對象時,這時如果 delete該基類指針,如果基類的析構函數不是虛函數,那么只會釋放基類自己的那部分,而派生類自己的那
部分得不到釋放,這是不安全的,如果子類的數據成員部分有動態分配的資源,那么就發生了內存泄漏,但是
可以將基類的析構函數定義為虛析構函數,這樣做即使是delete一個指向派生類對象的基類指針,也會先調用派生類的析構函數,在調用父類的析構函數。所以將析構函數設為虛函數總是正確的,后面會做實驗驗證