虚表和虚基表
- 虚表
- 虚基表
- 虚拟继承和虚函数都存在时的对象模型
虚表
我们知道,如果类中声明了的方法是用virtual进行修饰的,则说明当前这个方法要作为虚函数,而虚函数的存储和普通函数的存储是有区别的
当有虚函数声明时,编译器会创建一个虚函数表,将当前的虚函数按照声明次序放入虚函数表中,而这个虚函数表实际上就是一个函数指针数组,然后将当前这个虚函数表的地址放入对象模型的最起始位置。
class A
{
public:
virtual void fun1(){cout << "A::fun1()" << endl;}
virtual void fun2(){cout << "A::fun2()" << endl;}
virtual void fun3(){cout << "A::fun3()" << endl;}
int _a;
};
它对应的对象模型是这样的:
所以说,本质上虚函数表是一个函数指针数组,而对象模型中存放的是虚函数表的首地址,当我们需要调用虚函数时,传递对应的对象,就可以通过对象的地址获取对象的虚表指针,从而获取虚表,进而得到对应虚函数表中某个虚函数的地址,以此来进行调用(知道函数的入口地址,就可以调用对应的函数)
虚基表
我们知道,当出现菱形继承时,一定会出现对象模型中有多个基类对象成员。
//普通继承
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
上述代码中,D类对象中一定会存在B类和C类对象继承自A类对象的_a这个成员,这样就出现了两份_a成员,导致访问_a时出现二义性,并且随着继承深度和广度的增加,对象成员会越来越冗余。
为了解决这个问题,出现了虚拟继承。
//菱形虚拟继承
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
通过让B类和C类虚拟继承A类后,对象模型就从图1变成了图2
这样的转变,使得B类和C类虽然继承了A类,但是B类和C类中并没有存储A类的对象(基类对象只有一份,被存放在了整个对象模型的最后),除了子类新增之外,只有一个指针,这个指针就被称为虚基表指针。
虚基表指针所指向的是一个虚基表,对于B类ptr1这个虚基表,总大小为8个字节(32位系统下),前4个字节存储的是子类对象相对于自己起始位置的偏移量,(目前来看是0,当存在虚函数的虚拟继承时,就不是0了),后4个字节存储是子类对象相对于基类部分的偏移量。
ptr2指向C类这个对象的虚基表,总大小为8个字节(32位系统下),前4个字节存储的是子类对象相对于自己起始位置的偏移量,(目前来看是0,当存在虚函数的虚拟继承时,就不是0了),后4个字节存储是子类对象相对于基类部分的偏移量。
可以发现,虚表在整个类对象中只存储一份,也就是说一个类的不同对象共享同一份虚表。而虚基表有多份,取决于当前类是否虚拟继承了基类,若虚拟继承了基类,就会创建一个虚基表指针,指向一个虚基表。
虚拟继承和虚函数都存在时的对象模型
那么就存在另外一个问题,当虚拟继承和虚函数同时出现在继承体系中,对象模型又是什么样子呢?
class A
{
public:
virtual void fun1()
{
cout << "A::fun1()" << endl;
}
virtual void fun2()
{
cout << "A::fun2()" << endl;
}
int _a;
}
class B : virtual public A
{
public:
virtual void fun1()
{
cout << "B::fun1()" << endl;
}
virtual void fun3()
{
cout << "B::fun3()" << endl;
}
int _b;
};
class C : virtual public A
{
public:
virtual void fun2()
{
cout << "C::fun2()" << endl;
}
virtual void fun4()
{
cout << "C::fun4()" << endl;
}
int _c;
};
class D : public B, public C
{
public:
virtual void fun1()
{
cout << "D::fun1()" << endl;
}
virtual void fun2()
{
cout << "D::fun2()" << endl;
}
virtual void fun5()
{
cout << "D::fun5()" << endl;
}
int _d;
}
上述代码中,B类和C类都继承自A类,并且对A类中的虚函数进行了重写,同时也新增了虚函数。D类继承了B和C类,对B和C类中的虚函数进行了重写,同时也新增了虚函数。
那么当前在这个继承体系下,对象模型是什么样子呢?
其实不难想到,由于B类和C类都是虚拟继承,那么A类成员只会保留一份在最下方,同时B类和C类都会保存自己的虚基表指针,而D类由于是普通继承,按照顺序,新增的虚函数被放到B类的虚表中。
我们通过取地址发现,对象模型确实是上述的样子,但是在D类和A类之间,放了00000000作为对象分割区分(猜测)
请注意:当前的验证情况是在vs2019中进行验证的。
总结:当虚基表和虚表同时存在(虚拟继承和虚函数同时存在时),对象模型从整体上来说还是和虚拟继承相同(基类对象顺序按照声明的顺序从上到下排列,对象中没有祖父类的成员,祖父类成员被放到了模型的最下方)。但是由于有虚函数的存在,B类对A类的虚函数进行重写的虚函数在A类中直接修改,B类新增的虚函数被放到B类内部的虚表中,C类对A类的虚函数进行重写的虚函数在A类中直接修改,C类新增的虚函数被放到C类内部的虚表中。D类对B类和C类进行重写的虚函数直接在对应类中进行修改,D类新增的就直接放到B类的虚表中。
通过上述的描述,可以知道对于B类,C类和A类的虚表中存放的虚函数分别为:
而对于虚基表来说,表示的是子类对象相对于自己起始位置的偏移量,如果是B类,B类对象的起始位置已经有了一个虚表指针,那么虚基表中前四个字节要表示相对自己起始位置的偏移量就需要为-4,而后四个字节是相对于基类的偏移量是正常的计算方式。
对于B类C类的虚基表来说,其中的值为: