一. 什么为虚函数
简而言之,在一个类中,前面带有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函数是动态绑定的
动态绑定还有一个名字:多态(多态的基础是虚函数)