您的问题很有趣,但是我担心您作为第一个问题的目标太大了,所以如果您不介意,我将分几个步骤回答:)
免责声明:我不是编译器作者,虽然我确实研究过这个主题,但我的话应该谨慎行事。我会有不准确的地方。而且我对 RTTI 不是很精通。另外,由于这不是标准的,所以我描述的是可能性。
1.如何实现继承?
注意:我将忽略对齐问题,它们只是意味着块之间可以包含一些填充
让我们暂时将其排除在虚拟方法之外,并专注于如何实现继承,如下所示。
事实是继承和组合有很多共同点:
struct B { int t; int u; };
struct C { B b; int v; int w; };
struct D: B { int v; int w; };
看起来像:
B:
+-----+-----+
| t | u |
+-----+-----+
C:
+-----+-----+-----+-----+
| B | v | w |
+-----+-----+-----+-----+
D:
+-----+-----+-----+-----+
| B | v | w |
+-----+-----+-----+-----+
是不是很震惊:)?
然而,这意味着多重继承很容易弄清楚:
struct A { int r; int s; };
struct M: A, B { int v; int w; };
M:
+-----+-----+-----+-----+-----+-----+
| A | B | v | w |
+-----+-----+-----+-----+-----+-----+
使用这些图,让我们看看将派生指针转换为基指针时会发生什么:
M* pm = new M();
A* pa = pm; // points to the A subpart of M
B* pb = pm; // points to the B subpart of M
使用我们之前的图表:
M:
+-----+-----+-----+-----+-----+-----+
| A | B | v | w |
+-----+-----+-----+-----+-----+-----+
^ ^
pm pb
pa
pb
的地址与 的地址略有不同的事实pm
是编译器通过指针算法自动为您处理的。
2.如何实现虚拟继承?
虚拟继承很棘手:您需要确保单个V
(虚拟)对象将被所有其他子对象共享。让我们定义一个简单的菱形继承。
struct V { int t; };
struct B: virtual V { int u; };
struct C: virtual V { int v; };
struct D: B, C { int w; };
我将省略表示,并专注于确保在一个D
对象中,theB
和C
subparts 共享相同的子对象。怎么做到呢 ?
- 请记住,班级规模应该是恒定的
- 请记住,在设计时,B 和 C 都无法预见它们是否会一起使用
B
因此,找到的解决方案很简单:C
只为指向 的指针保留空间V
,并且:
- 如果你构建一个独立的
B
,构造函数将在堆上分配一个V
,这将被自动处理
- 如果您
B
作为 a 的一部分构建D
,则B
子部分将期望D
构造函数将指针传递给V
C
显然,同上。
在D
中,优化允许构造函数在对象中为V
right 保留空间,因为D
不虚拟继承自B
or C
,给出您显示的图表(尽管我们还没有虚拟方法)。
B: (and C is similar)
+-----+-----+
| V* | u |
+-----+-----+
D:
+-----+-----+-----+-----+-----+-----+
| B | C | w | A |
+-----+-----+-----+-----+-----+-----+
现在请注意,从B
to转换A
比简单的指针算术稍微棘手:您需要跟随指针B
而不是简单的指针算术。
不过,还有一个更糟糕的情况,向上转换。如果我给你一个指针,A
你怎么知道怎么回去B
?
在这种情况下,魔术是由 执行的dynamic_cast
,但这需要存储在某处的一些支持(即信息)。这就是所谓的RTTI
(运行时类型信息)。dynamic_cast
将首先通过一些魔术确定它A
是 a 的一部分D
,然后查询 D 的运行时信息以了解子对象内存储D
的位置。B
如果我们在没有B
子对象的情况下,它将返回 0(指针形式)或抛出bad_cast
异常(引用形式)。
3. 如何实现虚方法?
通常,虚拟方法通过每个类的 v-table(即指向函数的指针的表)和每个对象的 v-ptr 到该表来实现。这不是唯一可能的实现,并且已经证明其他实现可能更快,但是它既简单又具有可预测的开销(在内存和调度速度方面)。
如果我们采用一个简单的基类对象,带有一个虚方法:
struct B { virtual foo(); };
对于计算机来说,没有成员方法之类的东西,所以实际上你有:
struct B { VTable* vptr; };
void Bfoo(B* b);
struct BVTable { RTTI* rtti; void (*foo)(B*); };
当你派生自B
:
struct D: B { virtual foo(); virtual bar(); };
您现在有两种虚拟方法,一种是 overrides B::foo
,另一种是全新的。计算机表示类似于:
struct D { VTable* vptr; }; // single table, even for two methods
void Dfoo(D* d); void Dbar(D* d);
struct DVTable { RTTI* rtti; void (*foo)(D*); void (*foo)(B*); };
注意如何BVTable
和DVTable
如此相似(因为我们foo
之前提到过bar
)?这一点很重要!
D* d = /**/;
B* b = d; // noop, no needfor arithmetic
b->foo();
让我们将调用翻译成foo
机器语言(有点):
// 1. get the vptr
void* vptr = b; // noop, it's stored at the first byte of B
// 2. get the pointer to foo function
void (*foo)(B*) = vptr[1]; // 0 is for RTTI
// 3. apply foo
(*foo)(b);
这些 vptr 由对象的构造函数初始化,在执行 的构造函数时D
,发生了以下情况:
D::D()
B::B()
首先调用它的子部分
B::B()
初始化vptr
指向它的 vtable,然后返回
D::D()
初始化vptr
指向它的 vtable,覆盖 B 的
因此,vptr
这里指向的是D的vtable,因此foo
应用的是D的。因为B
它是完全透明的。
这里 B 和 D 共享同一个 vptr!
4.多继承中的虚拟表
不幸的是,这种共享并不总是可行的。
首先,正如我们所见,在虚拟继承的情况下,“共享”项在最终完整对象中的位置很奇怪。因此它有自己的 vptr。那是1。
其次,在多继承的情况下,第一个碱基与完整对象对齐,但第二个碱基不能对齐(它们都需要空间来存储数据),因此它不能共享它的 vptr。那是2。
第三,第一个基础与完整对象对齐,从而为我们提供与简单继承情况相同的布局(相同的优化机会)。那是3。
很简单,不是吗?