C++对象模型3——vptr的位置、手动调用虚函数、从汇编代码看普通调用和多态调用

一、vptr的位置

class test
{
public:
	int i;   
	virtual void testfunc() {}  
};

int main()
{
	test a;
	char* p1 = reinterpret_cast<char*>(&a);       
	char* p2 = reinterpret_cast<char*>(&(a.i));	 
	if (p1 == p2) { //如果a.i和a的地址相同,则成员变量i在a对象内存的开头位置,那么虚函数表指针在i的后面位置
		cout << "虚函数表指针位于对象内存的末尾" << endl;
	}
	else {
		cout << "虚函数表指针位于对象内存的开头" << endl; //本条件会成立
	}

	return 0;
}

test的对象模型就是这样的

 

二、手动调用虚函数

class Base
{
public:
	virtual void f() {
		cout << "Base::f()" << endl; 
	}
	virtual void g() { 
		cout << "Base::g()" << endl; 
	}
	virtual void h() { 
		cout << "Base::h()" << endl; 
	}
};
class Derive : public Base {
public:
	void g() {
		cout << "Derive::g()" << endl; 
	}
};

上述代码中基类和子类的虚函数表如下

可以看出,虚表中除了包含虚函数的函数指针,而且还包括了用于RTTI的typeinfo,而且vptr默认并不使指向虚表的第一个元素,而是指向第一个虚函数,而且虚函数的返回值从void变成了int

基于对象模型和虚表结构,可以手动获取vptr并手动调用虚函数

typedef void (*Func)();
int main()
{
	Derive* d = new Derive();       
	long* dvptr = reinterpret_cast<long*>(d);         
	long* dvfuncptr = reinterpret_cast<long*>(*dvptr);
	for (int i=0;i<3;++i) {
		((Func)dvfuncptr[i])();
	}

	Base* b = new Base();
	long* bvptr = (long*)b;
	long* bvfuncptr = (long*)(*bvptr);

	for (int i=0;i<3;++i) {
		((Func)bvfuncptr[i])();
	}

	return 0;
}

上述代码中,主要是这两行代码

long* dvptr = reinterpret_cast<long*>(d);         
long* dvfuncptr = reinterpret_cast<long*>(*dvptr);

因为vptr在类对象的最开头,占8个字节,所以将Derive*强转成long*,此时解引用就能得到对象的前8个字节的内容,也就是derive对象的vptr。因为有三个虚函数,每个虚函数指针占8个字节,所以再将vptr强转为long*,此时得到的结果就是第一个虚函数的地址。

根据虚表内容和分析结果:derive和base的对象内存模型如下

 

三、从汇编代码看普通调用和多态调用

int main()
{
	Derive d;
	Base b=d;
	b.g();
	Base *pb=new Derive();
	pb->g();

	return 0;
}

pb将能在编译时期做有以下两点:1、判定Base中函数的权限。2、根据访问权限,调用Base中的可用接口。也就是说,在main中,pb在编译期只能够调用Base的public接口。

第七行对g()的调用会被编译期转化为*(this->vptr[1])(this),在编译期可以确定的就是vptr指向了一个vtbl,而且知道g()在vtbl中的第二个位置(也就是知道g的索引),不确定的就是调用的到底是那个类vtbl中的g(),这一点需要在运行时确定。这就是所谓运行时多态

上述代码的对应的部分汇编代码如下

主要是三个红框中的call,第一个对应的是静态调用,直接调用Base::g(),Base::g()的地址在编译期就已经确定(已经被写死),是c72;第二个是创建Derive对象,也是在编译器进行的。而第三个最终调用的是64位累加寄存器RAX存储的函数指针,而RAX中的地址在编译器无法确定,是个变化的值。

所以,只能在运行期计算后得到RAX中的地址值。这就是为啥多态调用需要在运行期确定具体的调用函数,因为累加寄存器中RAX中的地址值是变化的,在运行期才能确定。也正因为静态调用的函数地址直接被写死,而多态调用需要在运行期确定具体的调用函数,所以,静态调用一般要比多态调用速度更快

这就是为什么在C++中被指定的对象的真实类型在每一个特定执行点之前,是无法在编译器解析的,只有通过指针和引用才能完成。相反,如果处理的只是一个类型的实例,它在编译时期就已经完全定义好了。

 

参考

《深度探索C++对象模型》

《C++新经典:对象模型》

 

欢迎大家评论交流,作者水平有限,如有错误,欢迎指出

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值