.1.虚基表
在前面继承的文章中,我们了解到为了避免菱形继承所导致的数据冗余,子类会将重复继承的部分合并为一份,放在类的最上或者最下面。但是这里引出一个问题是当我们通过父类指针访问子类对象,这是对于合并的部分,要如何确定位置呢?大家可能觉得合并的部分不是已经放在最后或者最上面了吗?但是这里如果我们使用不同的父类指针,偏移多少才能到底呢?因此需要虚基表记录父类对应的偏移量。
虚基表 是编译器为了解决多重继承场景下的菱形继承问题所设计的,虚基表(vbtable)通过记录虚基类实例的偏移量 来指示派生类如何访问唯一的虚基类实例。当子类通过多继承方式继承多个具有共同基类的父类时,如果不使用虚继承,子类会包含多分共同基类的数据,这会导致数据冗余。而是要虚继承,子类中只会包含一份共同基类的数据。

• 通过下面的简化菱形虚拟继承模型,我们可以看到,D对象中的B和C部分中分别包含一个指向虚基表的指针 ,B指向的虚基表中存储了B对象部分距离公共的A的相对偏移量距离 ,C指向的虚基表中存储了C对象部分距离公共的A的相对偏移量距离。这样公共的虚基类A部分在D对象中就只有一份了,这样就解决了数据冗余和二义性的问题。 • 通过B的对象模型,我们发现菱形虚拟继承中B和C的对象模型跟D保持的一致的方式去存储管理A,这样当B的这指针访问A时,无论B指针切片指向D对象,还是B指针直接指向B对象,访问A成员都是通过虚基表指针的方式查找到A成员再访问。
代码语言:javascript
AI代码解释
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._a = 3;
d._b = 4;
d._c = 5;
d._d = 6;
return 0;
}
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._a = 3;
d._b = 4;
d._c = 5;
d._d = 6;
B b;
b._a = 7;
b._b = 8;
// B的指针指向B对象
B *p2 = &b;
// B的指针指向D对象切片
B *p1 = &d;
// p1和p2分别对指向的_a成员访问修改
// 分析内存模型,我们发现B对象也使用了虚基表指向A成员的模型
// 所以打开汇编我们看到下面的访问_a的方式是一样的
p1->_a++;
p2->_a++;
return 0;
}


2. 单继承和多继承的虚函数表深入探索
2.1 单继承虚函数表深入探索
• vs编译器的监视窗口是经过特殊处理的,以它的角度给出了一个方便看的样子,但并不是本来的样子。多态部分我们讲了,虚函数指针都要放进虚函数表,这里我们通过监视窗口观察Derive对象,看不到func3和func4在虚表中,借助内存窗口可以看到一个地址,但是并不确认是不是func3和func4的地址。所以下面我们写了一份特殊代码,通过指针的方式,强制访问了虚函数表,调用了虚函数,确认继承中虚函数表中的真实内容。
代码语言:javascript
AI代码解释
class Base
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
void func5() { cout << "Derive::func5" << endl; }
private:
int b;
};
typedef void (*VFPTR)();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
// 注意如果是在g++下面,这里就不能用nullptr去判断访问虚表结束了
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%p,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
// 32位程序的访问思路如下:
// 需要注意的是如果是在64位下,指针是8byte,对应程序位置就需要进行更改
// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚
函数指针的指针数组,vs下这个数组最后面放了一个nullptr,g++ 下面最后没有nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚
表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 -
生成 - 清理解决方 案,再编译就好了。 VFPTR *vTable1 = (VFPTR *)(*(int *)&b);
PrintVTable(vTable1);
VFPTR *vTable2 = (VFPTR *)(*(int *)&d);
PrintVTable(vTable2);
return 0;
}
2.2 多继承虚函数表深入探索
代码语言:javascript
AI代码解释
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2
{
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
typedef void (*VFPTR)();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%p,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR *vTableb1 = (VFPTR *)(*(int *)&d);
PrintVTable(vTableb1);
VFPTR *vTableb2 = (VFPTR *)(*(int *)((char *)&d + sizeof(Base1)));
PrintVTable(vTableb2);
Base1 *p1 = &d;
p1->func1();
Base2 *p2 = &d;
p2->func1();
d.func1();
return 0;
}
• 跟前面单继承类似,多继承时Derive对象的虚表在监视窗口也观察不到部分虚函数的指针。所以我们一样可以借助上面的思路强制打印虚函数表。 • 需要注意的是多继承时,Derive中同时继承了Base1和Base2,内存中先继承的对象在前面,并且Derive中包含的Base1和Base2各有一张虚函数表,通过观察我们发现Derive没有重写的虚函数func3,选择放在先继承的Base1的虚函数表中。 • 另外需要注意的是,有些细心的读者发现Derive对象中重写的Base1虚表的func1地址和重写Base2虚表的func1地址不一样,这是为什么呢?这个问题还比较复杂。需要我们分别对这两个函数进行多态调用,并翻阅对应的汇编代码进行分析,才能捋清楚问题所在。这里简单说一个结论就是本质Base2虚表中func1的地址并不是真实的func1的地址,而是封装过的func1地址,因为Base2指针p2指向Derive时,Base2部分在中间位置,切片时,指针会发生偏移,那么多态调用p2->func1()时,p2传递给this前需要把p2给修正回去指向Derive对象,因为func1是Derive重写的,里面this应该是指向Derive对象的。
