虽然不建议多继承,但有些场景还是很适合多继承的。同时,这部分知识虽然实际不会用到,但是我们也可以从中学习到语言的强大,C++作为当时的老大哥,当然因为前沿所以有一大堆坑,我们也可以从C++之父解决这些问题中学习到一些思想
那么,就开始今天的学习
我们先构建一个多继承模型
有一个Base1父类和Base2父类,两个父类都有fun1和fun2两个虚函数
Derive公有继承这两个类,但Derive只重写fun1,并且自己新增一个虚函数fun3
问:Derive有几个虚表
?新增的虚函数放在哪
?
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;
};
首先我们通过监视窗口可以得到第一个问题的答案
我们看到,Derive内部是有两个虚表的,一个继承Base1,一个继承Base2。所有Derive共有两张虚表,但是新增的func3我们还是无法通过监视窗口看到,这时我们同样可以使用上篇博客多态结论证明中使用的打印虚表的函数
typedef void(*VF_PTR)();
void Print(VF_PTR table[])
{
for (int i = 0; table[i]; i++)
{
printf("[%d]:%p->", i, table[i]);
VF_PTR f = table[i];
f();
}
cout << endl;
}
这里不再解释,感兴趣的可以查看上篇博客。
但是迎面而来的是一个问题,打印第一张虚表,我们可以直接获取对象的前四个字节,但是第二张虚表呢?是不是需要偏移呢?偏移多少呢?怎么偏印呢?
第一张虚表我们可以直接访问前四个字节
得到虚表指针
Print((VF_PTR*)(*(int*)(&d)));
第二章虚表,我们就需要偏移指针获得了,偏移多少呢?偏移一个Base1的大小,注意我们需要先强转成char*,这样访问范围是1字节,加上Base1的大小才是正确的偏移量
Print((VF_PTR*)(*(int*)( (char*)&d +sizeof(Base1) )));
另外,我们除了手动让指针偏移以外,我们还可以这样操作。父类指针指向子类,会进行切片,会自动指向子类的父类部分,这样就自动实现指针的偏移了。
但是仔细观察一下,我们会发现一个问题
这里我们通过虚表打印出了函数指针指向的虚函数地址,同时取出函数指针,并调用。这里func1调用的也是子类重写的虚函数,但是这两个虚函数的地址却不一样
。
这是否代表有两份重写的func1虚函数呢?
答案是,没有,只有一份
以下通过汇编角度解释
从这两次调用func1的汇编指令,我们发现,最终都会跳转到同一个地址的函数
,所以重写的func1函数确实只有一份
,但为什么func2的地址不一样?又为什么中间还多一步的跳转呢?
我们观察得到,Base2的汇编跳转,是为了多执行一个指令
ecx是this指针的寄存器
这一步汇编指令的作用是将当前的this指针往上移动8个字节。
因为满足了多态,父类指针指向子类对象,但调用的是子类的函数,所以this指针
要指向子类对象的首地址
,但是父类指针会发生切片
,指针指向的是子类的父类部分
,所以要偏转
回到子类对象的首地址。
那为什么Base1不需要偏转回去呢?这是因为Base1是先继承的
,其地址就在子类的第一部分
,所以Base1的父类指针刚好指向子类的首地址。
静态绑定又称为
前期绑定(早绑定)
,在程序编译期间
确定了程序的行为,也称为静态多态。比如:函数重载
函数模板本质也是函数重载
,为什么函数模板不能分离编译
,就是因为需要在编译时
确定参数类型
,不然不会生成函数的地址
,运行时就会产生链接错误
。
动态绑定又称后期绑定(晚绑定),是在程序
运行期间
,根据具体拿到的类型
确定程序的具体行为,调用具体的函数,也称为动态多态。
此部分为补充知识,可以不掌握,因为菱形继承本来就不建议实现,在此基础上的多态更是不会使用,这样的模型也复杂,所以可以不掌握。
我们构建这样一些类
class A
{
public:
virtual void func1()
{}
public:
int _a;
};
class B : virtual public A
{
public:
virtual void func1()
{}
virtual void func2()
{}
public:
int _b;
};
class C : virtual public A
{
public:
virtual void func1()
{}
virtual void func3()
{}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void func1()
{
}
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
根据菱形虚拟继承
的知识,为了解决数据冗余和二义性,所以在B和C中会创建虚基表
,存储偏移量
,同时因为虚继承,多态的实现,A,B,C,D中都有虚表
接下来,我们通过内存窗口
查看 d 对象的具体存储结构
A,B,C的第一个地址都是虚表指针
B,C的第二个地址是虚基表指针
因为B和C同时重写了A的虚函数,所以B和C各自原本的虚表应该是自己的虚函数的函数指针,但因为都虚继承的A的虚函数,所以函数指针并不是在B和C各自的虚表,A的内容会被放在对象的最下面,变为公共部分
,所以只能有一个
重写的虚函数,这时候用哪个都不合适,所以此时要求D必须也重写A的虚函数
。
所以此时,B和C的虚表内,其实只有自己新增的虚函数的函数指针
而B和C的虚基表是这样的
第一个是ff ff ff fc 代表-4,也是偏移量,刚好移到虚表的位置
第二个偏移量,是偏移到A这个公共部分的。
本篇文章是对多态的一些比较偏的知识的笔记,掌握要求不深,感谢您的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。