一、多重继承的对象内存模型
class Base1
{
public:
virtual void f() {
cout << "base1::f()" << endl;
}
virtual void g() {
cout << "base1::g()" << endl;
}
};
class Base2
{
public:
virtual void h() {
cout << "base2::h()" << endl;
}
virtual void i() {
cout << "base2::i()" << endl;
}
};
class Derived : public Base1, public Base2
{
public:
virtual void f() {
cout << "derived::f()" << endl;
}
virtual void i() {
cout << "derived::i()" << endl;
}
virtual void mh() {
cout << "derived::mh()" << endl;
}
virtual void mi() {
cout << "derived::mi()" << endl;
}
virtual void mj() {
cout << "derived::mj()" << endl;
}
};
上述代码的虚表结构如下
通过Derive的虚表可以知道:derive对象中有两个vptr(如果一个类含有多个含有虚函数的基类,那么类对象中的vptr的个数与含有虚函数的基类个数相同),第一个指向Derived::f,第二个指向Base2::h,所以,derive对象的sizeof是16。这个也很好理解,因为vptr是编译器添加的成员变量,所以继承的时候,子类就会继承基类的vptr,上述代码中的88...thunk to Derived::i()中的thunk就是用于第二基类调用 Derived::i()时的this指针调整
手动调用上述代码中的虚函数
typedef void(*Func)(void);
int main()
{
Derived ins; //定义一个子类对象
Base1& b1 = ins;
Base2& b2 = ins;
Derived& d = ins;
long* pderived1 = (long*)(&ins);
long* vptr1 = (long*)(*pderived1);
for (int i=0;i<6;++i) {
((Func)(vptr1[i]))();
}
cout<<"--------"<<endl;
long* pderived2 = ++pderived1;
long* vptr2 = (long*)(*pderived2);
for (int i=0;i<2;++i) {
((Func)(vptr2[i]))();
}
return 0;
}
pderived1自增后,就能获取到第二个vptr,所以,在derive对象中,这两个vptr是连续分布的(按照继承顺序先后连续分布)。
第一段输出结果中既调用了derive的函数,也调用了base1的函数,所以derive和base1共用一个vptr(子类和第一个被继承的基类共用一个vptr)。
derive对象的内存模型如下
上图中的最后中的thunk的意义在于:1、以适当的offset值调整this指针(图中的-8就是offset),可以跳转到对应的虚函数。
比如下面的这段代码
Base2 *pb2 = new Derived();
pb2->i();
因为base2在derived对象中的最底下,所以,为了能正确调用derived::i(),必须首先根据offset调整this指针(上面的例子需要将this上移8个格),使this指针指向derived对象,然后跳转到derived::i(),实现正确的多态调用
二、vptr与vtbl的创建与重置的时机
虽然vptr和vtbl都是为了在运行时进行多态调用,但是二者的创建都是在编译器在编译期做的。根据文章2中的分析可知:vptr是在创建对象时编译器创建的并在构造函数中给vptr进行初始化,让其指向对应的vtbl(更具体一点是在基类或者虚基类的构造函数执行完之后,在初始化列表被展开之前指向vtbl)。vtbl也是在编译期创建并将虚函数的地址填入了vtbl
三、不要在含有虚函数的类的构造函数中调用memset
class X
{
public:
X(){
memset(this, 0, sizeof(X));
cout << "X类的构造函数被执行" << endl;
}
virtual void virfunc(){
cout << "虚函数virfunc()执行了" << endl;
}
};
int main()
{
X* pX0 = new X();
pX0->virfunc();
return 0;
}
上述代码之所以出现段错误的原因就是memset将vptr的值清空,导致调用虚函数的时候找不到虚表中虚函数的地址,对应的汇编代码如下
可见,在X的构造函数中,先对vptr进行赋值,然后又将vptr进行memset。
此外,也不要在拷贝构造函数中调用memcpy,因为用一个对象初始化另一个对象时,memcpy也会将右值中的vptr覆盖掉,有可能出现问题
实际上,上述结论可以进一步扩展,如果编译器在编译期间为了实现某些机制而自动生成了一些内部成员(包括虚函数、虚继承中的vptr),都不可以在构造函数中使用memcpy和memset
参考
《深度探索C++对象模型》
《C++新经典:对象模型》
欢迎大家评论交流,作者水平有限,如有错误,欢迎指出