多重虚拟继承和类型转换的虚拟表和虚拟指针

10

我对vptr和内存中对象的表示有点困惑,希望你能帮助我更好地理解这个问题。

  1. 考虑类B继承自A,两个类都定义了虚函数f()。根据我的理解,类B的一个对象在内存中的表示形式如下:[ vptr | A | B ],并且vptr所指向的vtbl包含。我也理解将对象从B转换为A除了忽略对象结尾的B部分之外并不做任何事情。这是正确的吗?这种行为不会出现问题吗? 我们希望类型为A的对象执行方法而不是方法。

  2. 系统中是否有与类数量相同的vtable?

  3. 继承两个或更多类的类的vtable看起来会是什么样子?C类的对象在内存中的表示形式是什么?

  4. 与第3题相同,但使用虚拟继承。

3个回答

16
以下内容适用于GCC(似乎也适用于LLVM link),但你使用的编译器也可能适用。所有这些都是实现相关的,不受C++标准控制。然而,GCC编写了自己的二进制标准文档,Itanium ABI
我试图用更简单的语言解释虚拟表的基本概念,作为我article about virtual function performance in C++的一部分,你可能会发现有用。以下是你的问题的答案:
  1. A more correct way to depict internal representation of the object is:

    | vptr | ======= | ======= |  <-- your object
           |----A----|         |
           |---------B---------|
    

    B contains its base class A, it just adds a couple of his own members after its end.

    Casting from B* to A* indeed does nothing, it returns the same pointer, and vptr remains the same. But, in a nutshell, virtual functions are not always called via vtable. Sometimes they're called just like the other functions.

    Here's more detailed explanation. You should distinguish two ways of calling member function:

    A a, *aptr;
    a.func();         // the call to A::func() is precompiled!
    aptr->A::func();  // ditto
    aptr->func();     // calls virtual function through vtable.
                      // It may be a call to A::func() or B::func().
    

    The thing is that it's known at compile time how the function will be called: via vtable or just will be a usual call. And the thing is that the type of a casting expression is known at compile time, and therefore the compiler chooses the right function at compile time.

    B b, *bptr;          
    static_cast<A>(b)::func(); //calls A::func, because the type
       // of static_cast<A>(b) is A!
    

    It doesn't even look inside vtable in this case!

  2. Generally, no. A class can have several vtables if it inherits from several bases, each having its own vtable. Such set of virtual tables forms a "virtual table group" (see pt. 3).

    Class also needs a set of construction vtables, to correctly distpatch virtual functions when constructing bases of a complex object. You can read further in the standard I linked.

  3. Here's an example. Assume C inherits from A and B, each class defining virtual void func(), as well as a,b or c virtual function relevant to its name.

    The C will have a vtable group of two vtables. It will share one vtable with A (the vtable where the own functions of the current class go is called "primary"), and a vtable for B will be appended:

    | C::func()   |   a()  |  c()  || C::func()  |   b()   |
    |---- vtable for A ----|        |---- vtable for B ----| 
    |--- "primary virtual table" --||- "secondary vtable" -|
    |-------------- virtual table group for C -------------|
    

    The representation of object in memory will look nearly the same way its vtable looks like. Just add a vptr before every vtable in a group, and you'll have a rough estimate how the data are laid out inside the object. You may read about it in the relevant section of the GCC binary standard.

  4. Virtual bases (some of them) are laid out at the end of vtable group. This is done because each class should have only one virtual base, and if they were mingled with "usual" vtables, then compiler couldn't re-use parts of constructed vtables to making those of derived classes. This would lead to computing unnecessary offsets and would decrease performance.

    Due to such a placement, virtual bases also introduce into their vtables additional elements: vcall offset (to get address of a final overrider when jumping from the pointer to a virtual base inside a complete object to the beginning of the class that overrides the virtual function) for each virtual function defined there. Also each virtual base adds vbase offsets, which are inserted into vtable of the derived class; they allow to find where the data of the virtual base begin (it can't be precompiled since the actual address depends on the hierarchy: virtual bases are at the end of object, and the shift from beginning varies depending on how many non-virtual classes the current class inherits.).

汪,我希望我没有引入太多不必要的复杂性。无论如何,您可以参考原始标准或自己编译器的任何文档。


2
  1. 我认为这是正确的。 如果您正在使用A指针,则只需要A提供的内容以及可能从A虚函数表中可用的B函数实现(根据编译器和层次结构复杂性,可能有几个虚函数表)。
  2. 我会说是,但这取决于编译器实现,所以你不需要真正了解它。
  3. 请继续阅读。

我建议阅读Multiple Inheritance Considered Useful,这是一篇长文,但它通过详细解释C++中继承的工作原理使事情变得更加清晰(图像链接不起作用,但它们在页面底部可用)。


-1
  1. 如果对象B继承自A,则B的内存表示如下:

    • A虚表指针
    • A特定变量/函数
    • B虚表指针
    • B特定变量/函数/覆盖

    如果你有B* b = new B(); (A)b->f(),那么:

    • 如果f被声明为虚函数,则调用B的实现,因为b是B类型
    • 如果f没有被声明为虚函数,则在调用时不会在vtable中查找正确的实现,将调用A的实现。
  2. 每个对象都有自己的虚表(不要认为这是理所当然的,因为我需要进行研究)

  3. 请参考this,了解处理多重继承时的虚表布局示例

  4. 请参阅this,了解菱形继承和虚表表示的讨论


编译器会尽可能地重用基类子对象的虚表指针(vptr),避免额外的vptr,除非必要。 - curiousguy
(A)b 不是一个指针转换,你正在构造另一个对象。 - curiousguy

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