如果基类与派生类中有同名成员函数,根据类型兼容规则,当使用基类指针或基类引用操作派生类对象时,只能调用基类的同名函数。如果想要使用基类指针或基类引用派生类中的成员函数,就需要虚函数解决,虚函数是实现多态的基础。
一.虚函数简介,函数的直接调用和间接调用
虚函数的声明方式是在成员函数的返回值类型前面加上virtual关键字,格式如下:
class 类名{ 权限控制符: virtual 函数返回值类型 函数名(参数表); 其他成员... };
我们通过一个实例观察:
#include "stdafx.h" class Base{ public: void Function_1(){ printf("Function_1...\n"); } virtual void Function_2(){ printf("Function_2..."); } }; int main(int argc, char* argv[]) { Base base; base.Function_1(); base.Function_2(); return 0; }
我们来到反汇编观察这两个函数的调用过程:
base.Function_1(); 8D 4D FC lea ecx,[ebp-4] E8 06 FF FF FF call @ILT+25(Base::Function_1) (0040101e) base.Function_2(); 8D 4D FC lea ecx,[ebp-4] E8 F9 FE FF FF call @ILT+20(Base::Function_2) (00401019)
我们看到,通过类的对象调用函数时,不管是构造函数还是虚函数,都是先传入一个this指针,然后通过call的方式调用函数(硬编码为E8)。
我们再来通过指针来调用函数看看:
#include "stdafx.h" class Base{ public: void Function_1(){ printf("Function_1...\n"); } virtual void Function_2(){ printf("Function_2..."); } }; int main(int argc, char* argv[]) { Base base; Base* p; p->Function_1(); p->Function_2(); return 0; }
我们来到反汇编查看函数调用过程:
p->Function_1(); 8B 4D F8 mov ecx,dword ptr [ebp-8] E8 06 FF FF FF call @ILT+25(Base::Function_1) (0040101e) p->Function_2(); 8B 45 F8 mov eax,dword ptr [ebp-8] 8B 10 mov edx,dword ptr [eax] 8B F4 mov esi,esp 8B 4D F8 mov ecx,dword ptr [ebp-8] FF 12 call dword ptr [edx]
我们可以清晰地看到,调用Function_1函数的时候是通过call指令调用的,而调用Function_2的时候是通过call一个地址来调用的,而且硬编码为FF。
这里就是我们介绍的间接调用和直接调用:**直接调用为直接call函数地址,硬编码为E8,间接调用是先call一个地址,通过地址中存储的值再调用函数,硬编码为FF,这个有点像我们之前PE里面讲的IAT表的作用。
二.深入了解虚函数调用(虚函数表)
我们通过sizeof函数来看看虚函数是否占用类的空间:
我们先来写一个虚函数看看:
#include "stdafx.h" class Base{ public: int a; int b; void Function_1(){ printf("Function_1...\n"); } virtual void Function(){ printf("Function_2...\n"); } }; int main(int argc, char* argv[]) { printf("%d",sizeof(Base)); return 0; }
我们可以看到程序输出窗口输出了12,而我们的类中本来就有两个int类型数据,我们之前讲过构造函数是不占用类的内存的,所以证明虚函数占用了类中4个字节空间。
我们再来写两个虚函数试试:
#include "stdafx.h" class Base{ public: 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[]) { printf("%d",sizeof(Base)); return 0; }
我们看到程序输出窗口还是输出了12,说明不管是几个虚函数,都只占用类中4个字节空间。
那么多出来这四个字节空间到底是什么?
我们通过反汇编查看:
这里通过指针分别调用了两个虚函数,更好地理解
#include "stdafx.h" class Base{ public: int a; int b; Base(int a,int b){ this->a = a; this->b = b; } 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* p; Base base(1,2); p = (Base*)&base; p->Function_1(); p->Function_2(); p->Function_3(); return 0; }
注意这里我们给对象中a和b赋了初值,我们来到反汇编窗口来观察一下函数调用过程:
p->Function_1(); 8B 4D FC mov ecx,dword ptr [ebp-4] E8 26 38 FF FF call @ILT+35(Base::Function_1) (00401028) p->Function_2(); 8B 4D FC mov ecx,dword ptr [ebp-4] 8B 11 mov edx,dword ptr [ecx] 8B F4 mov esi,esp 8B 4D FC mov ecx,dword ptr [ebp-4] FF 12 call dword ptr [edx] 3B F4 cmp esi,esp E8 2B 39 FF FF call __chkesp (00401140) p->Function_3(); 8B 45 FC mov eax,dword ptr [ebp-4] 8B 10 mov edx,dword ptr [eax] 8B F4 mov esi,esp 8B 4D FC mov ecx,dword ptr [ebp-4] FF 52 04 call dword ptr [edx+4]
我们着重观察调用两个虚函数的指令:FF 12 call dword ptr [edx]
FF 52 04 call dword ptr [edx+4]
我们可以发现它俩的函数地址好像只差了4个字节。
我们来到Memory窗口来看看:
我们可以观察到,调用虚函数的时候,将this指针指向的第一个内容当作地址,然后取出该地址中的值,通过调用这个地址,完成了调用函数。
我们给出一张图,相信大家能够更好地理解:
在这里我们的this指针指向类,第一个成员为虚函数表,之后的是类的属性,在虚函数表中,函数的排序是按照我们定义虚函数的顺序排列的。
四.子类函数的虚函数表
我们来创建一个父类,再创建一个子类,子类继承父类的函数,那么在子类的虚函数表中,是怎样排序的呢?
我们来试验一下:
#include "stdafx.h" class Base{ public: int a; int b; /*Base(int a,int b){ this->a = a; this->b = b; }*/ void Function_1(){ printf("Function_1...\n"); } virtual void Function_2(){ printf("Function_2...\n"); } virtual void Function_3(){ printf("Function_3...\n"); } }; class Base2:public Base{ public: int c; int d; Base2(int a,int b,int c,int d){ this->a = a; this->b = b; this->c = c; this->d = d; } virtual void Function_4(){ printf("Function_4...\n"); } }; int main(int argc, char* argv[]) { Base2* p; Base2 ase(1,2,3,4); p = (Base2*)&ase; p->Function_1(); p->Function_2(); p->Function_3(); p->Function_4(); return 0; }
我们在父类中定义了Function_1,Function_2,Function_3函数,其中Function_2,Function_3为虚函数
创建子类,继承父类,并定义一个虚函数Function_4,我们来看看调用这些虚函数的过程:
我们通过反汇编来查看:
p->Function_1(); 8B 4D FC mov ecx,dword ptr [ebp-4] E8 84 FF FF FF call @ILT+5(Base::Function_1) (0040100a) p->Function_2(); 8B 4D FC mov ecx,dword ptr [ebp-4] 8B 11 mov edx,dword ptr [ecx] 8B F4 mov esi,esp 8B 4D FC mov ecx,dword ptr [ebp-4] FF 12 call dword ptr [edx] 3B F4 cmp esi,esp E8 D7 01 00 00 call __chkesp (00401270) p->Function_3(); 8B 45 FC mov eax,dword ptr [ebp-4] 8B 10 mov edx,dword ptr [eax] 8B F4 mov esi,esp 8B 4D FC mov ecx,dword ptr [ebp-4] FF 52 04 call dword ptr [edx+4] 3B F4 cmp esi,esp E8 C3 01 00 00 call __chkesp (00401270) p->Function_4(); 8B 45 FC mov eax,dword ptr [ebp-4] 8B 10 mov edx,dword ptr [eax] 8B F4 mov esi,esp 8B 4D FC mov ecx,dword ptr [ebp-4] FF 52 08 call dword ptr [edx+8]
首先我们能够看到,不管是调用父类虚函数还是子类虚函数,都是通过虚函数表间接调用的,我们来看看子类this指针中的情况:
我们仍然能够看到,this第一个成员为虚函数表地址,后面的是子类属性的值。
我们再来看看虚函数表的情况:
结合前面的返回编,我们可以得出结论:
子类虚函数表中的排序:父类的虚函数在前,并且按照定义的顺序排序,之后才是子类的虚函数(也是按照定义的顺序排序)。