一、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++新经典:对象模型》
欢迎大家评论交流,作者水平有限,如有错误,欢迎指出