在尝试更深入地分析C ++的继承机制时,我偶然发现了以下示例:
#include<iostream>
using namespace std;
class Base {
public:
virtual void f(){
cout << "Base.f" << endl;
}
};
class Left : public virtual Base {
};
class Right : public virtual Base{
public:
virtual void f(){
cout << "Right.f" << endl;
}
};
class Bottom : public Left, public Right{
};
int main(int argc,char **argv)
{
Bottom* b = new Bottom();
b->f();
}
以上,不知何故,编译并调用Right :: f()。我看到编译器可能会发生什么,它知道有一个共享的Base对象,并且Right覆盖f(),但实际上,在我的理解中,应该有两个方法:Left::f()
(继承来自Base::f()
)和Right::f()
,其覆盖Base::f()
。现在,我认为,基于Bottom继承了两个单独的方法,两个都有相同的签名,应该有冲突。
有没有人可以解释C ++的哪些规范细节处理这种情况以及它是如何从低级别的角度来处理的呢?
答案 0 :(得分:4)
在 dreaded diamond 中有一个基础,两个中间对象从中派生出来,然后第四个类型用中间层中两种类型的多重继承关闭菱形。
您的问题似乎是在前一个示例中声明了多少f
个函数?,答案是一个。
让我们从简单的基础和派生的线性层次结构的例子开始:
struct base {
virtual void f() {}
};
struct derived : base {
virtual void f() {}
};
在此示例中,声明了一个f
,其中有两个覆盖base::f
和derived::f
。在类型为derived
的对象中,最终的覆盖为derived::f
。值得注意的是,两个f
函数都代表一个具有多个实现的函数。
现在,回到原始示例,在右侧的行上,Base::f
和Right::f
与被覆盖的相同函数的方式相同。因此,对于Right
类型的对象,最终的覆盖是Right::f
。现在,对于类型为Left
的最终对象,最终的覆盖为Base::f
,因为Left
不会覆盖该函数。
当菱形被关闭,并且因为继承是virtual
时,只有一个Base
对象,它声明了一个f
函数。在第二级继承中,Right
使用自己的实现覆盖该函数,这是最派生类型Bottom
的最终覆盖。
您可能希望在标准之外查看此内容,并了解编译器实际如何实现它。编译器在创建Base
对象时,会向虚拟表中添加隐藏指针vptr
。虚拟表保存指向 thunks 的指针(为简单起见,假设该表保存了指向函数的最终重写符的指针,[1])。在这种情况下,Base
对象将不包含任何成员数据,只包含指向包含指向函数Base::f
的指针的表的指针。
当Left
扩展Base
时,会为Left
创建一个新的vtable,并且该vtable中的指针将设置为此级别的f
的最终覆盖,顺便提一下Base::f
所以两个vtables中的指针(忽略trampolin)跳转到相同的实际实现。当正在构造类型为Left
的对象时,首先初始化Base
子对象,然后在Left
成员(如果有)Base::vptr
的初始化之前进行初始化指针被更新以引用Left::vtable
(即Base
中存储的指针指的是为Left
定义的表。)
在钻石的另一面,为Right
创建的vtable包含一个 thunk ,最终调用Right::f
。如果要创建类型为Right
的对象,则会发生相同的初始化过程,Base::vptr
将指向Derived::f
。
现在我们到达最终对象Bottom
。同样,为类型Bottom
生成vtable,并且vtable与所有其他类型的情况一样,包含表示f
的单个条目。编译器分析继承的层次结构并确定Right::f
覆盖Base::f
,并且左分支上没有等效覆盖,因此在Bottom
的vtable中,指针代表{{1} }指f
。同样,在构建Right::f
对象期间,Bottom
会更新以引用Base::vptr
的vtable。
如您所见,所有四个vtable都有一个Bottom
条目,程序中有一个单 f
,即使每个vtable中存储的值都是不同(最终的改写不同)。
[1] thunk 是一小段代码,如果需要,它会调整f
指针(多重继承通常意味着需要它),然后将调用转发给实际覆盖。在单继承的情况下,this
指针不需要更新, thunk 消失,vtable中的条目直接指向实际函数。