虚函数表
虚函数表
#include<stdlib.h> #include<stdio.h> struct Base1 { private: int x; int y; public: void Fun() { } }; struct Base2 { private: int x; int y; public: virtual void Fun() { } }; struct Base3 { public: int x; int y; private: virtual void Fun1() { } virtual void Fun2() { } }; int main() { printf("%d\n%d\n%d\n", sizeof(Base1),sizeof(Base2),sizeof(Base3)); return 0; }
代码中定义了三个类,分别为Base1,Base2,Base3。Base1中没有定义虚函数,Base2定义了1个虚函数,Base3定义了2个虚函数,并且用打印了三个类的大小

三个类的大小分别为8,8,12,由此可见,当类中定义了虚函数,不管定义了多少个,编译器都为类中增加了4个字节,而这四个字节正是存贮的是虚函数表的地址。
还要注意的是,虚函数表的地址位于对象的首地址,即首地址所对应的前四个字节所存的就是虚函数的地址。
虚函数表的间接调用
一般的对象调用虚函数是都是直接调用,而对象指针调用虚函数使用的是间接调用,这时候就用到了虚函数表。
下面代码所对应的汇编代码
Base2 base2;
Base2* pbase2 = &base2;
base2.Fun();
pbase2->Fun();
-----------------------------------
base2.Fun();
00FD18D0 lea ecx,[base2]
00FD18D3 call Sub::Sub (0FD1424h)
pbase2->Fun();
00FD18D8 mov eax,dword ptr [pbase2]
00FD18DB mov edx,dword ptr [eax]
00FD18DD mov esi,esp
00FD18DF mov ecx,dword ptr [pbase2]
00FD18E2 mov eax,dword ptr [edx]
00FD18E4 call eax
-------------------------------------
当base2调用Fun时,直接跳转到了函数所在的地址。
而当pbase2调用Fun时,先通过对象的首地址得到虚函数表的地址,在通过该地址得到虚函数的地址,在进行跳转。
同类不同对象的虚函数表相同
struct Base { private: int x; public: virtual void Fun() { } }; int main() { Base base1; Base base2; printf("虚函数表地址为:%X\n", *(int*)&base1); printf("虚函数表地址为:%X\n", *(int*)&base1); return 0; }
![]()
base1和base2的虚函数表的地址相同,也就是说,每个类只有一张虚函数表。
单继承无函数覆盖
struct Base
{ public: virtual void Function_1() { } virtual void Function_2() { } virtual void Function_3() { } }; struct Sub:Base { public: virtual void Function_4() { } virtual void Function_5() {
} virtual void Function_6() { } };
int main()
{
Base base;
Sub sub;
printf("虚函数表地址为:%X\n", *(int*)&base);
for (int i = 0; i < 3; i++)
{
printf("%X\n", *((int*)(*(int*)&base) + i));
}
printf("********\n");
printf("虚函数表首地址为:%X\n", *(int*)&sub);
for (int i = 0; i < 6; i++)
{
printf("%X\n", *((int*)(*(int*)&sub) + i));
}
return 0;
}

可以看出,派生类继承基类的虚函数函数,并不是数据的复制,而是直接调用基类的虚函数,派生类拥有自己的虚函数表
单继承有函数覆盖
#include<stdlib.h> #include<stdio.h> 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() { Base base; Sub sub; printf("虚函数表地址为:%X\n", *(int*)&base); for (int i = 0; i < 3; i++) { printf("%X\n", *((int*)(*(int*)&base) + i)); } printf("********\n"); printf("虚函数表首地址为:%X\n", *(int*)&sub); for (int i = 0; i < 3; i++) { printf("%X\n", *((int*)(*(int*)&sub) + i)); } return 0; }

其中Fun1,Fun2都已经在派生类中被重写,与基类的不同。
多重继承有函数覆盖
#include<stdlib.h>
#include<stdio.h>
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;
}

如上图所示,当直接基类有两个是,派生类的虚函数表也有两个。以此类推,派生类的虚函数表的个数取决与直接基类的个数。
函数的多态
首先我们要理解一个概念:绑定——将调用代码和地址联系起来,即代码知道要跳转的地址。
绑定分为两种,编译期绑定——在编译的时候确定地址,动态绑定——在运行的时候确定地址
#include<stdlib.h> #include<stdio.h> class Base { public: void Fun1() { printf("Bass 1"); } virtual void Fun2() { printf("Bass 2"); } }; class Sub :Base { public: void Fun1() { printf("Sub 1"); } void Fun2() { printf("Sub 2"); } }; void Test(Base* pb) { pb->Fun1(); pb->Fun2(); } int main() { Base base; Test(&base); return 0; }

代码中定义了两个类,基类Base和派生类Sub,其中Fun2为虚函数,而Fun1不是。Test函数是以基类指针作为参数,众所周知,基类的指针可以指向基类,也可以指向自身的派生类。
让我们来看一下函数Test的反汇编代码:
其中Fun1的call指令已经确定了要跳转的地址——Base::Fun1,这也就是编译期绑定,或是前期绑定;而Fun2没有确定。在Fun2中,mov eax,dword ptr [pb]是将对象首地址放入eax中。
mov edx,dword ptr [eax],是将虚函数表的地址放入edx中,而最后的mov,dword ptr [edx]是将虚函数表中的第一个地址放入eax,然后call eax。由此可见,
根据传进来的参数不同,虚函数表也有可能会不同,因此这实现了在运行阶段将代码与跳转地址联系在了一起,这也就是动态绑定。
多态实现了一个属性表现了不同的行为,多态也可以成为动态绑定。
虚函数的动态绑定可以通过虚函数表来实现,因为根据之前所提到的虚函数可以间接寻址,或是间接调用,所以可以不用再编译期间确定函数要跳转的地址。
在PE文件结构中的IAT表也有相同的行为。

浙公网安备 33010602011771号