1.虚函数:间接调用
1.使用base调用函数
首先我们观察通过base对象来调用正常函数和带有virtual的函数的反汇编
此时我们可以看到ebp-4即this指针的地址作为参数传参。带virtual函数和普通函数都是使用E8 call
2.使用指针调用参数
可以看到带virtual函数使用FF call调用,查看反汇编call的是dword ptr [edx],表示此时访问的是this指针中第一个dword字节中的内容。
可以得出结论:
使用指针调用虚函数时,是使用间接调用的方式(FF call),call的是this指针中第一个dword字节的内容
2.深入虚函数调用
1.当类中有虚函数时,观察大小的变化
可以观察到有虚函数的类的大小要比之前的大4字节,我们再测试多个虚函数时,同样只是大于四字节。此时我们可以猜测这四字节可能是一个地址,此地址存储了一个虚函数表。
3.打印虚函数表
我们已经猜测多出的四个字节是虚函数的地址,因此我们需要通过调用该地址来调用函数来印证是否为真。
通过访问this指向第一个dword字节作为地址的(所谓虚函数表)的第一个内容(虚函数地址)作为函数指针可以调用虚函数。因此我们可以确定
对象地址处的第一个4字节的内容是一个虚函数表的地址,此地址指向虚函数表,虚函数表以4字节存储着连续的虚函数的地址。
4.作业
1、单继承无函数覆盖(打印Sub对象的虚函数表)
2、单继承有函数覆盖(打印Sub对象的虚函数表)
1.
#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_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()
{
Sub sub;
typedef void (*pFunc)(void);
pFunc fp=NULL;
for(int i=0;i<6;i++)
{
fp=(pFunc)*(((int*)*(int*)&sub)+i);
fp();
}
return 0;
}
我们可以看到当虚函数没有重写时,首先调用的是父类的虚函数,然后再调用子类的。
2.
#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()
{
Sub sub;
typedef void (*pFunc)(void);
pFunc fp=NULL;
for(int i=0;i<6;i++)
{
fp=(pFunc)*(((int*)*(int*)&sub)+i);
fp();
}
return 0;
}
此时运行的结果为:如果有重写情况时,先调用的是子类的虚函数,再调用父类的。