基类和派生类是否共享一个虚表?我之前应聘的时候,一些知名互联网公司的面试官很肯定的说是的。虽然我告诉他们,派生类对象中指向虚表的指针值和基类对象中指向虚表的指针值是不一样的,所以不应该共享同一个虚表,但是面试官还是很肯定的说,他们共享同一个虚表。所以我有点困惑,就来这里找答案。
基类和派生类是否共享一个虚表?我之前应聘的时候,一些知名互联网公司的面试官很肯定的说是的。虽然我告诉他们,派生类对象中指向虚表的指针值和基类对象中指向虚表的指针值是不一样的,所以不应该共享同一个虚表,但是面试官还是很肯定的说,他们共享同一个虚表。所以我有点困惑,就来这里找答案。
这可能取决于实现,但至少 gcc 和 clang 都不会让基类和派生类共享 vtable。每个类将有一个 vtable(然后在该类的所有实例之间共享)。如果每个类没有一个 vtable,那么使用 vtable 实现运行时多态性的目的就会落空。
当然这是一个实现问题,但除非你禁用 RTTI,否则基类和派生类绝对不能具有相同的 vptr,因为它们也用于
typeid
和dynamic_cast
,并且类的行为并不相同。那么,为了共享 vtable,他们要么拥有单独的 vtable ptrs 和 typeinfo ptrs(每个对象都有额外的指针开销,这是一个糟糕的实现选择),要么让 vptr 指向每个类的 typeinfo 对象,然后让该对象包含指向共享 vtable 的指针,这意味着虚函数调用又增加了一个间接级别,这也是另一个糟糕的实现选择。
没有任何合理的实现会共享 vtable。
这是英语细节很重要的地方,非英语母语人士可能会在这里吃亏。(在 Staging Ground 中,很明显作者不是英语母语人士。幸运的是,这个问题已经解决了。)也许在面试过程中出现了一些沟通错误?特别是,我看到了“在基类对象中”,这句话的措辞有点不太好。当措辞稍微不对时,人们往往会认为是纠正了。然而,在这种情况下,我看到了两种可能的纠正方法,不幸的是,这导致了两个截然不同的问题。
如果将“the”改为“a”,则比较的是派生类对象中的指针和基类对象(而不是子对象)中的指针。从这个角度来看,问题是基类和派生类是否可以共享虚函数表。也就是说,两个类是否可以有一个 vtable?
如果将“对象”改为“子对象”,那么比较的就是单个对象中的两个指针。每个派生类对象都包含一个基类子对象。基类子对象需要包含指向 vtable 的指针,以便可以通过指向基类的指针调用虚函数。同样,派生类对象也必须包含指向 vtable 的指针。从这个角度来看,问题是这两个指针是否必须具有相同的值。也就是说,这个对象是否只有一个 vtable?
类和(多个)vtable
虽然 vtable 的实现并不标准化,但通常每个类都有自己的虚函数表。基类有一个,派生类有一个不同的。通常,会有两个 vtable,每个类一个。
但从理论上讲,情况并非如此。如果派生类不重写任何函数,则基类和派生类的 vtable 可能相同。此外,如果运行时类型信息被抑制(这可能会限制 的功能
dynamic_cast
),则编译器可以将这两个表优化为一个表。请记住,该理论是针对一种相当特殊的情况进行的优化。我不知道它是否真的被实现了。因此,可以合理地得出结论,不同的类有不同的 vtable,至少在任何重要的情况下都是如此。
对象及其(一个)vtable
在对象中,派生类通常使用基类子对象中已经存在的虚拟表指针。由于表的巧妙安排,基类可以查看派生类的表并“看到”适用于基类的部分。(这类似于指向基类的指针可以指向派生对象并“看到”基类子对象的方式。)在这种情况下,vpointer 的值相同,因为只有一个 vpointer。
在更复杂的情况下,一个对象中可能会有多个虚拟表指针,尤其是在多重继承的情况下(即一个类有多个基类,而不是一个基类有一个基类)。如果有多个基类具有虚拟函数,则每个基类都需要自己的虚拟表指针。此外,这些虚拟指针实际上不可能具有相同的值,因为每个基类都独立确定其虚拟表的结构。因此,派生类虚拟指针不能与每个基类虚拟指针具有相同的值。(不过,它将匹配其中一个,至少在我所知道的实现中是这样。)
但是,即使基类的 vpointer 不同,对象仍然只有一个 vtable。将派生类 vtable 视为由基类 1 函数、基类 2 函数等组成。每个基类子对象都有一个指向派生类表的 vpointer。因此,原则上,vpointer 指向同一张表,只是偏移量不同。
总之,对于给定的对象,只有一个虚函数表。派生类和它的一个基类将就此表的地址达成一致。如果还有其他基类,它们的 vpointer 将与派生类的 vpointer 偏移,但仍指向同一张表。
如果您没有重写单个方法,那么表将会相同并因此被共享。
如果您没有覆盖任何内容,但在派生类中添加了更多虚拟方法,则基类可以使用派生类 vtable 的开头。并且所有这些仅在 vtable 仅用于方法分派时才有效。
否则,vtable 一定不同。