一个使用单一/多重继承的类对象将有多少个vptr?

6

一个类(child)继承了一个单一继承的基类和多重继承的base1和base2,通常需要多少个vptrs?如果一个对象有几个单一继承和多重继承,如何确定它有多少个vptrs?虽然标准没有规定vptrs,但我想知道实现如何实现虚函数。

2个回答

7

你为什么在意呢?简单的答案是“足够”,但我猜你想要更完整的解释。

这不是标准的一部分,所以任何实现都可以自由地做出决定,但一个普遍的经验法则是,在使用虚表指针的实现中,作为第零近似,对于动态分发,你需要最多与添加新虚拟方法到层次结构中的类数目相同的虚拟表指针数量。 (在某些情况下,虚拟表可以被扩展,并且基类型和派生类型共享一个单一的“vptr”)

    // some examples:
    struct a { void foo(); };           // no need for virtual table
    struct b : a { virtual foo1(); };   // need vtable, and vptr
    struct c : b { void bar(); };       // no extra virtual table, 1 vptr (b) suffices
    struct d : b { virtual bar(); };    // 1 vtable, d extends b's vtable

    struct e : d, b {};                 // 2 vptr, 1 for the d and 1 for b                                      
    struct f : virtual b {};            // 1 vptr, f reuse b's vptr to locate subobject b
    struct g : virtual b {};            // 1 vptr, g reuse b's vptr to locate subobject b
    struct h : f, g {};                 // 2 vptr, 1 for f, 1 for g
                                        // h can locate subobject b using f's vptr

基本上,每个需要自己的动态分派(不能直接重用父级)的类型的子对象都需要自己的虚拟表和vptr。
实际上,编译器将不同的vtable合并成一个单一的vtable。当在b的函数集中添加一个新的虚拟函数时,编译器将通过将新插槽附加到vtable的末尾来将两个潜在的表合并为一个单一的表,因此的vtable将是的vtable的扩展版本,其末尾有额外的元素,以维护二进制兼容性(即可以将 vtable解释为 vtable以访问中可用的方法),而对象将具有单个指针。

在多重继承的情况下,情况变得有些复杂,因为每个基类都需要与完整对象的子对象的布局相同,就好像它是一个单独的对象一样,因此会有额外的vptr指向完整对象的vtable中的不同区域。
最后,在虚拟继承的情况下,情况变得更加复杂,可能会有多个vtable用于相同的完整对象,并且随着构造/销毁的演变而更新vptr(vptr始终在构造/销毁演变时更新,但是如果没有虚拟继承,则vptr将指向基类的vtable,而在虚拟继承的情况下,同一类型可能会有多个vtable)。

2
"struct d : b { virtual bar(); }; // 需要额外的虚函数表,需要 b.vptr 和 d.vptr" 我认为没有编译器会在非虚基类中引入超过一个 vptr。 - curiousguy

5

注意事项

关于vptr/vtable的任何内容都没有具体说明,因此对于详细信息,这将取决于编译器,但几乎每个现代编译器都处理简单情况(我用“几乎”来防止万一)。

请您注意。

对象布局:非虚拟继承

如果您从基类继承,并且它们有一个vptr,则自然会在您的类中拥有同样数量的继承vptr

问题是:当编译器向已经有继承vptr的类添加vptr时,会发生什么?

编译器将尝试避免添加冗余的vptr:

struct B { 
    virtual ~B(); 
};

struct D : B { 
    virtual void foo(); 
};

这里B有一个vptr,因此D不会拥有自己的vptr,而是重用现有的vptr;B的vtable通过添加一个foo()项进行扩展。D的vtable是从B的vtable“派生”的,伪代码如下:

struct B_vtable { 
    typeinfo *info; // for typeid, dynamic_cast
    void (*destructor)(B*); 
};

struct D_vtable : B_vtable { 
    void (*foo)(D*); 
};

再次声明:这是一个真实虚函数表的简化版,以便更好地理解。

虚继承

对于非虚单一继承,各个编译器之间几乎没有太多差别。但是对于虚继承来说,不同编译器之间的差异就比较大了。

struct B2 : virtual A {
};

需要将 B2* 转换为 A*,因此一个 B2 对象必须提供以下功能:

  • 有一个 A* 成员
  • 有一个整数成员 offset_of_A_from_B2
  • 使用其vptr,在vtable中存储 offset_of_A_from_B2

通常情况下,一个类不会重用其虚基类的vptr(但是在非常特殊的情况下可以)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接