vcall的深入理解

什么是vcall,来自GPT的描述

在 C++ 中,vcall(虚函数调用)是通过虚表(vtable)来实现的,用于在运行时动态地调用适当的虚函数。虚函数是指在基类中声明为 virtual 的函数,并在派生类中进行重写的函数。
当通过基类指针或引用调用虚函数时,会使用 vcall 机制确定要调用的实际函数。具体步骤如下:

  1. 对于包含虚函数的类,编译器在对象的内存布局中添加了一个指向虚表的指针,通常称为虚指针(vptr)。虚表是一个函数指针数组,保存了该类及其派生类的虚函数的地址。
  2. 虚表中的每个条目对应于类的虚函数,每个条目是一个函数指针,指向实际的虚函数实现。
  3. 在编译时,编译器为每个类生成了一个唯一的虚表(vtable),并将其与对象的虚指针关联起来。
  4. 当通过基类指针或引用调用虚函数时,程序会首先访问对象的虚指针,然后通过虚指针找到对应的虚表。
  5. 通过虚表中的函数指针可以动态地确定具体需要调用的虚函数的地址,然后执行相应的函数代码。

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前面都用了类名来区分

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值