什么是vcall,来自GPT的描述
在 C++ 中,vcall(虚函数调用)是通过虚表(vtable)来实现的,用于在运行时动态地调用适当的虚函数。虚函数是指在基类中声明为 virtual 的函数,并在派生类中进行重写的函数。
当通过基类指针或引用调用虚函数时,会使用 vcall 机制确定要调用的实际函数。具体步骤如下:
- 对于包含虚函数的类,编译器在对象的内存布局中添加了一个指向虚表的指针,通常称为虚指针(vptr)。虚表是一个函数指针数组,保存了该类及其派生类的虚函数的地址。
- 虚表中的每个条目对应于类的虚函数,每个条目是一个函数指针,指向实际的虚函数实现。
- 在编译时,编译器为每个类生成了一个唯一的虚表(vtable),并将其与对象的虚指针关联起来。
- 当通过基类指针或引用调用虚函数时,程序会首先访问对象的虚指针,然后通过虚指针找到对应的虚表。
- 通过虚表中的函数指针可以动态地确定具体需要调用的虚函数的地址,然后执行相应的函数代码。
vcall 的主要目的是实现多态性,即在运行时根据对象的真实类型来调用适当的函数。它使得在编写具有继承和多态性质的代码时更加灵活和方便。
需要注意的是,vcall 机制增加了额外的开销,包括访问虚指针和虚表的开销,以及动态查找并调用正确的虚函数的开销。然而,这种开销通常是可以接受的,并且可以通过编译器的优化和处理来最小化其影响。
通过使用 vcall,C++ 支持了面向对象编程中的动态多态性,使得基类指针或引用可以根据对象的实际类型来调用适当的函数,从而实现了运行时的动态分派。
什么时候会使用到vcall
在使用函数指针调用成员虚函数的时候会使用到vcall
使用函数指针调用非虚构函数会使用vcall嘛?
#include <iostream>
#include <cstdio>
class Base
{
public:
Base(){}
virtual ~Base(){}
void fun1(){}
};
class Drive : public Base
{
public:
Drive(){}
virtual ~Drive(){}
};
int main()
{
Base *base = new Drive();
void (Base::*tempFun1)(void) = &Base::fun1;
(base->*tempFun1)();
printf("%p", &Base::fun1);
return 0;
}
汇编:
22: void (Base::*tempFun1)(void) = &Base::fun1;
00E56C50 mov dword ptr [tempFun1],offset Base::fun1 (0E51483h)
23: (base->*tempFun1)();
00E56C57 mov esi,esp
00E56C59 mov ecx,dword ptr [base]
00E56C5C call dword ptr [tempFun1]
00E56C5F cmp esi,esp
00E56C61 call __RTC_CheckEsp (0E5129Eh)
24: printf("%p", &Base::fun1);
00E56C66 push offset Base::fun1 (0E51483h)
24: printf("%p", &Base::fun1);
00E56C6B push offset string "%p" (0E59C50h)
00E56C70 call _printf (0E51460h)
00E56C75 add esp,8
很明显,并没有使用到vcall
在虚函数中使用vcall
#include <iostream>
#include <cstdio>
class Base
{
public:
Base(){}
virtual ~Base(){}
virtual void fun1() {}
};
class Drive : public Base
{
public:
Drive(){}
virtual ~Drive(){}
};
int main()
{
Base *base = new Drive();
void (Base::*tempFun1)(void) = &Base::fun1;
(base->*tempFun1)();
printf("%p", &Base::fun1);
return 0;
}
汇编
22: void (Base::*tempFun1)(void) = &Base::fun1;
000C6C50 mov dword ptr [tempFun1],offset Base::`vcall'{4}' (0C1497h)
23: (base->*tempFun1)();
000C6C57 mov esi,esp
000C6C59 mov ecx,dword ptr [base]
000C6C5C call dword ptr [tempFun1]
000C6C5F cmp esi,esp
000C6C61 call __RTC_CheckEsp (0C129Eh)
24: printf("%p", &Base::fun1);
000C6C66 push offset Base::`vcall'{4}' (0C1497h)
24: printf("%p", &Base::fun1);
000C6C6B push offset string "%p" (0C9C50h)
000C6C70 call _printf (0C1460h)
000C6C75 add esp,8
可以清楚的看见在第22行、24行下面都调用了vcall,可以把vcall理解成虚函数表,它vcall{4},那么就是从第二个4字节开始的
这时猜测虚函数表的位置跟我定义虚函数的顺序有关系,于是修改代码
class Base
{
public:
Base(){}
virtual void fun1() {}
virtual ~Base(){}
};
汇编
22: void (Base::*tempFun1)(void) = &Base::fun1;
00CD6C50 mov dword ptr [tempFun1],offset Base::`vcall'{0}' (0CD149Ch)
23: (base->*tempFun1)();
00CD6C57 mov esi,esp
00CD6C59 mov ecx,dword ptr [base]
00CD6C5C call dword ptr [tempFun1]
00CD6C5F cmp esi,esp
00CD6C61 call __RTC_CheckEsp (0CD129Eh)
24: printf("%p", &Base::fun1);
00CD6C66 push offset Base::`vcall'{0}' (0CD149Ch)
24: printf("%p", &Base::fun1);
00CD6C6B push offset string "%p" (0CD9C50h)
00CD6C70 call _printf (0CD1460h)
00CD6C75 add esp,8
现在就变成了vcall{0}了,那么猜想完全正确。vtable是会有多个的,那么多个他会怎么调用呢
多继承中vcall
#include <iostream>
#include <cstdio>
class Base
{
public:
Base(){}
virtual void fun1() {}
virtual ~Base(){}
};
class Base2
{
public:
Base2(){}
virtual ~Base2(){}
virtual void fun2() {};
};
class Drive : public Base, public Base2
{
public:
Drive(){}
virtual ~Drive(){}
};
int main()
{
Drive *d = new Drive();
void (Base::*tempFun1)(void) = &Base::fun1;
(d->*tempFun1)();
printf("%p\n", &Base::fun1);
void (Base2::*tempFun2)(void) = &Base2::fun2;
(d->*tempFun2)();
printf("%p\n", &Base2::fun2);
return 0;
}
汇编
30: void (Base::*tempFun1)(void) = &Base::fun1;
00063400 mov dword ptr [tempFun1],offset Base::`vcall'{0}' (061302h)
31: (d->*tempFun1)();
00063407 mov esi,esp
00063409 mov ecx,dword ptr [d]
0006340C call dword ptr [tempFun1]
0006340F cmp esi,esp
00063411 call __RTC_CheckEsp (0612BCh)
32: printf("%p\n", &Base::fun1);
00063416 push offset Base::`vcall'{0}' (061302h)
0006341B push offset string "%p\n" (06AC70h)
00063420 call _printf (061050h)
00063425 add esp,8
33: void (Base2::*tempFun2)(void) = &Base2::fun2;
00063428 mov dword ptr [tempFun2],offset Base2::`vcall'{4}' (0612C6h)
34: (d->*tempFun2)();
0006342F cmp dword ptr [d],0
00063433 je main+0E3h (063443h)
00063435 mov eax,dword ptr [d]
00063438 add eax,4
0006343B mov dword ptr [ebp-10Ch],eax
00063441 jmp main+0EDh (06344Dh)
00063443 mov dword ptr [ebp-10Ch],0
0006344D mov esi,esp
0006344F mov ecx,dword ptr [ebp-10Ch]
00063455 call dword ptr [tempFun2]
00063458 cmp esi,esp
0006345A call __RTC_CheckEsp (0612BCh)
35: printf("%p\n", &Base2::fun2);
0006345F push offset Base2::`vcall'{4}' (0612C6h)
35: printf("%p\n", &Base2::fun2);
00063464 push offset string "%p\n" (06AC70h)
00063469 call _printf (061050h)
0006346E add esp,8
观察发现,每次调用vcall前面都用了类名来区分