要解决的基本问题是,如果您将指向最派生类型的指针强制转换为指向其基数之一的指针,则该指针必须引用内存中的地址,该类型的每个成员都可以通过不知道派生类型。对于非虚拟继承,这通常是通过具有精确的布局来实现的,而这又是通过包含一个基类子对象然后添加派生类型的额外位来实现的:
struct base { int x; };
struct derived : base { int y };
派生的布局:
--------- <- base & derived start here
x
---------
y
---------
如果你添加第二个派生类型和一个最派生类型(同样,没有虚拟继承),你会得到类似:
struct derived2 : base { int z; };
struct most_derived : derived, derived 2 {};
使用此布局:
--------- <- derived::base, derived and most_derived start here
x
---------
y
--------- <- derived2::base & derived2 start here
x
---------
z
---------
如果您有一个most_derived
对象并且您绑定了一个类型的指针/引用,derived2
它将指向标有 的行derived2::base
。现在,如果从 base 继承是虚拟的,那么应该有一个base
. 为了讨论,假设我们天真地删除了第二个base
:
--------- <- derived::base, derived and most_derived start here
x
---------
y
--------- <- derived2 start here??
z
---------
现在的问题是,如果我们获得一个指向derived
它的指针,它的布局与原始布局相同,但是如果我们试图获得一个指向布局的指针,derived2
那么布局就会不同,并且代码中的代码derived2
将无法定位该x
成员。我们需要做一些更聪明的事情,这就是指针发挥作用的地方。通过添加一个指向每个虚拟继承对象的指针,我们得到了这个布局:
--------- <- derived starts here
base::ptr --\
y | pointer to where the base object resides
--------- <-/
x
---------
同样对于derived2
。现在,以额外的间接为代价,我们可以x
通过指针定位子对象。当我们可以most_derived
使用单个基础创建布局时,它可能如下所示:
--------- <- derived starts here
base::ptr -----\
y |
--------- | <- derived2
base::ptr --\ |
z | |
--------- <--+-/ <- base
x
---------
现在编写代码derived
并derived2
了解如何访问基本子对象(只需取消引用base::ptr
成员对象),同时您有一个base
. 如果任一中间类中的代码访问x
它们,它们都可以通过这样做来访问this->[hidden base pointer]->x
,这将在运行时解析到正确的位置。
这里重要的一点是在derived
/derived2
层编译的代码可以与该类型的对象或任何派生对象一起使用。如果我们编写了第二个most_derived2
继承顺序颠倒的对象,那么它们的布局y
和z
可以交换,并且从指向derived
或derived2
子对象的指针到子对象的偏移量base
将不同,但访问代码x
仍然相同: 取消引用你自己的隐藏基指针,保证如果一个方法 inderived
是最终的覆盖器,base::x
那么无论最终布局如何,它都会找到它。