您的问题很有趣,但是我担心您作为第一个问题的目标太大,所以如果您不介意,我将分几个步骤回答:)
免责声明:我不是编译器作者,虽然我确实研究过这个主题,但我的话应该谨慎行事。我会有不准确的地方。而且我对 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 对象中,B 和 C 子部分共享相同的子对象。怎么办?
- 请记住,类大小应该是恒定的
- 请记住,在设计时,B 和 C 都无法预见它们是否会一起使用
因此找到的解决方案很简单:B 和C 只为指向V 的指针保留空间,并且:
- 如果你构建一个独立的
B,构造函数会在堆上分配一个V,会自动处理
- 如果您将
B 构建为D 的一部分,B 子部分将期望D 构造函数将指针传递给V 的位置
显然,C 同上。
在D 中,优化允许构造函数在对象中为V 保留空间,因为D 不会从B 或C 虚拟继承,如图所示(虽然我们还没有虚拟方法)。
B: (and C is similar)
+-----+-----+
| V* | u |
+-----+-----+
D:
+-----+-----+-----+-----+-----+-----+
| B | C | w | A |
+-----+-----+-----+-----+-----+-----+
现在请注意,从 B 转换为 A 比简单的指针运算稍微复杂一些:您需要跟随 B 中的指针而不是简单的指针运算。
还有一个更糟糕的情况,向上转换。如果我给你一个指向A 的指针,你怎么知道如何回到B ?
在这种情况下,魔术是由dynamic_cast 执行的,但这需要在某处存储一些支持(即信息)。这就是所谓的RTTI(运行时类型信息)。 dynamic_cast 将首先通过某种魔法确定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(); };
你现在有两个虚拟方法,一个覆盖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。
很简单,不是吗?