C++虚函数
Posted unknowcodemaker
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++虚函数相关的知识,希望对你有一定的参考价值。
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一个指向派生类对象的基类指针,也会先调用派生类的析构函数,在调用父类的析构函数。所以将析构函数设为虚函数总是正确的,后面会做实验验证
以上是关于C++虚函数的主要内容,如果未能解决你的问题,请参考以下文章