用于多个虚拟继承和类型转换的虚拟表和虚拟指针

Art*_*ium 10 c++ multiple-inheritance virtual-inheritance vtable vptr

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

  1. 考虑B从中继承A并定义虚函数f().从我了解到的记忆B类对象的表示是这样的:[ vptr | A | B ]vtblvptr指向包含B::f().我也明白,从铸造对象BA什么都不做,除了忽略B在对象的端部.这是真的吗?这种行为不对吗?我们希望类型的对象A执行A::f()方法而不是B::f().

  2. 是否有一些vtables在系统中的类的数量?

  3. 一个将如何vtable类,由两个或多个类继承的是什么样子?如何将C的对象表示在内存中?

  4. 与问题3相同,但具有虚拟继承.

P S*_*ved 16

以下适用于GCC(对于LLVM 链接似乎也是如此),但对于您正在使用的编译器也可能如此.所有这些都依赖于实现,并且不受C++标准的约束.但是,GCC编写了自己的二进制标准文档Itanium ABI.

我尝试用更简单的单词解释虚拟表的基本概念,作为我在C++中关于虚函数性能的文章的一部分,您可能会发现它很有用.以下是您的问题的答案:

  1. 描述对象内部表示的更正确方法是:

    | vptr | ======= | ======= |  <-- your object
           |----A----|         |
           |---------B---------|
    
    Run Code Online (Sandbox Code Playgroud)

    B 包含它的基类 A,它只是在结束后添加了几个自己的成员.

    从实际上B*转换A*为什么都没有,它返回相同的指针,并vptr保持不变.但是,简而言之,虚函数并不总是通过vtable调用.有时他们被称为就像其他功能一样.

    这里有更详细的解释.你应该区分两种调用成员函数的方法:

    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().
    
    Run Code Online (Sandbox Code Playgroud)

    问题在于它在编译时是如何调用函数的:通过vtable或者只是通常的调用.事实是,在编译时已知铸造表达式的类型,因此编译器在编译时选择正确的函数.

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

    在这种情况下它甚至看不到vtable!

  2. 一般来说,没有.如果一个类继承自几个基类,每个类都有自己的vtable,那么它可以有多个vtable.这组虚拟表形成"虚拟表组"(参见第3页).

    类还需要一组构造vtable,以在构造复杂对象的基础时正确地分散虚函数.您可以在我链接的标准中进一步阅读.

  3. 这是一个例子.假设C从继承AB,每类中定义virtual void func(),以及a,bc虚函数相关的实至名归.

    C将有两个虚函数表的虚函数表组.它将与A(当前类的自身功能称为"主要"的vtable 共享一个vtable )和B附加的vtable :

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

    对象在内存中的表示看起来与其vtable看起来几乎相同.只需vptr在组中的每个vtable之前添加一个,您就可以粗略估计数据在对象内的布局方式.您可以在GCC二进制标准的相关部分中阅读它.

  4. 虚拟基础(其中一些)在vtable组的末尾布局.这样做是因为每个类应该只有一个虚拟基础,如果它们与"通常"的vtable混合在一起,那么编译器就不能重新使用构造的vtable的一部分来创建派生类.这将导致计算不必要的偏移并且会降低性能.

    由于这样的放置,虚拟基础还在其vtable中引入了额外的元素:vcalloffset(当从指针跳转到完整对象内的虚拟基础到覆盖虚函数的类的开头时获取最终覆盖的地址)对于那里定义的每个虚函数.此外,每个虚拟基础都会添加vbase偏移量,这些偏移量将插入到派生类的vtable中; 它们允许查找虚拟基础数据的开始位置(它不能预编译,因为实际地址取决于层次结构:虚拟基础位于对象的末尾,并且从开始的转变取决于非虚拟的数量当前类继承的类.).

Woof,我希望我没有引入太多不必要的复杂性.在任何情况下,您可以参考原始标准或您自己的编译器的任何文档.