vtable 包含该类的每个虚拟函数在已知偏移量处的地址。
[备注:在实践中,与常规类不同,vtable 具有负偏移量的成员,很像数组中间的指针。这只是一个不会太大改变实现自由的约定。无论如何,唯一的问题是在 vtable 中放置信息是由约定(ABI)规定的,编译器遵循相同的规则为多态类生成兼容的代码。]
当派生类中有附加函数时会发生什么?(不仅仅是从基类“继承”的函数)
一旦您接受指向结构的指针既指向整个对象又指向其第一个成员的想法,您就会认为指向派生类的指针指向适当位于偏移量零处的基类。因此,您可以拥有完全相同的指针值,表示为 a void*
,可以在此约定下用于派生对象或基类,用于单继承。
现在,您可以将其应用于任何数据结构,甚至应用于实际上不是表(相同类型的元素数组或可以以相同方式解释的值的数组)而是记录(不相关类型的对象或意义); 您可以看到,此类派生类的 vtable 可以以完全相同的方式从其唯一基类的 vtable 派生。
(请注意,如果将 C++ 编译为 C,则在执行此类操作时可能会遇到类型别名规则。当然汇编没有这样的问题,也没有天真的编译的“高级汇编程序”C。)
因此,对于单继承,基类被集成并优化到派生类中:
- 对于实例的数据成员(类类型)
- 对于虚函数成员,它是 vtable 的数据成员(或者你想象的元类的成员)。
请注意,将 base 放置在 offset 0 处允许您将 vtable base 放置在 offset 0 处,这反过来又允许您使用相同的 vptr 但并不暗示它;相反,与基础共享 vptr 意味着基础 vtable 位于偏移量零(vtable 布局 = 元类级别),因此基础必须位于偏移量零(数据成员布局 = 类级别)。
多重继承实际上是单继承加,因为一个类总是被视为特权:它放置在偏移量零处,因此指针相同,因此 vtable 可以放置在偏移量零处(因为指针相同);其他基地,并非如此。
正如我们所看到的,除了一个继承的多态类之外,所有继承的多态类都放置在多重继承中的非零偏移处。每个都在派生类中携带一个额外的“继承”vptr;该(隐藏)指针成员必须由任何派生构造函数正确填充。
这些额外的 vptr 用于发生在非零偏移量的基类,因此必须调整指向继承基的指针(添加一个正常量以转换为基指针,删除它以转换回来)。编译器需要生成代码来执行隐式转换是一件小事(将整数转换为浮点类型是一项复杂得多的任务);但这里的转换this
是在给定基类型上的函数调用和降落在作为基类或派生类中的覆盖器的函数中:不同之处在于调整取决于仅对类(实例)已知的函数覆盖元类型)。所以vptr需要指向不同的vtable信息:知道如何处理这些基到派生指针转换的人。
作为“元类型”的实例,vtables 拥有自动调整所有指针的所有信息。(这些取决于所涉及的特定类类型,而不取决于其他因素。)
所以在实现层面,两种类型的继承是:
- 零偏移继承;共享 vptr;在某些 vtable 和 ABI 描述中称为主要基类;
- 任意偏移继承;拥有另一个 vptr;称为二级基类。
这是基本的东西。虚拟继承在实现层面要微妙得多,甚至主要的概念也不是很清楚,因为虚拟基只能在一些更多的派生类中成为派生类的“主要”!