【问题标题】:Virtual tables and virtual pointers for multiple virtual inheritance and type casting用于多重虚拟继承和类型转换的虚拟表和虚拟指针
【发布时间】:2011-03-20 11:28:05
【问题描述】:

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

  1. 考虑 B 继承自 A 并且都定义了虚函数 f()。从我了解到的 B 类对象在内存中的表示形式如下所示:[ vptr | A | B ] 以及vptr 指向的vtbl 包含B::f()。我还了解到,将对象从B 转换为A 除了忽略对象末尾的B 部分外,没有任何作用。这是真的吗?这种行为是不对的吗?我们希望A 类型的对象执行A::f() 方法而不是B::f()

  2. 系统中有vtables作为类数吗?

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

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

【问题讨论】:

    标签: c++ multiple-inheritance vtable virtual-inheritance vptr


    【解决方案1】:

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

    作为我的article about virtual function performance in C++ 的一部分,我试图用更简单的语言解释虚拟表如何布局的基本概念,您可能会觉得这很有用。以下是您的问题的答案:

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

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

      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().
      

      问题在于,在编译时知道函数将如何被调用:通过 vtable 或只是普通调用。问题是强制转换表达式的类型在编译时是已知的,因此编译器会在编译时选择正确的函数。

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

      在这种情况下,它甚至不会查看 vtable 内部!

    2. 一般来说,不会。如果一个类从多个基类继承,则可以有多个 vtable,每个基类都有自己的 vtable。这样的一组虚拟表形成了一个“虚拟表组”(见 pt. 3)。

      类还需要一组构造虚表,以便在构造复杂对象的基础时正确分配虚函数。您可以在the standard I linked 中进一步阅读。

    3. 这是一个例子。假设C 继承自AB,每个类定义virtual void func(),以及与其名称相关的abc 虚函数。

      C 将有一个包含两个 vtable 的 vtable 组。它将与A共享一个vtable(当前类的自己的函数所在的vtable称为“primary”),并附加一个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 -------------|
      

      对象在内存中的表示形式几乎与其 vtable 的外观相同。只需在组中的每个 vtable 之前添加 vptr,您就可以粗略估计数据在对象内的布局方式。您可以在 GCC 二进制标准的relevant section 中了解它。

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

      由于这样的放置,虚拟基也引入了附加元素到它们的 vtables 中:vcall offset(当从指针跳转到完整对象内的虚拟基到类的开头时,获取最终覆盖器的地址覆盖虚拟功能)对于在那里定义的每个虚拟功能。每个虚基也增加了vbase的偏移量,这些偏移量被插入到派生类的vtable中;它们允许找到虚拟基础数据的开始位置(它不能被预编译,因为实际地址取决于层次结构:虚拟基础位于对象的末尾,并且从开始的移位取决于有多少非虚拟当前类继承的类。)。

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

    【讨论】:

      【解决方案2】:
      1. 这对我来说似乎是正确的。好像您使用的是 A 指针并没有错,您只需要 A 提供的内容以及可能从 A vtable 获得的 B 函数实现(可能有多个 vtable,具体取决于编译器和层次结构的复杂性)。
      2. 我会说是的,但它依赖于编译器实现,因此您不必真正了解它。
      3. 和 4. 进一步阅读。

      我建议阅读Multiple Inheritance Considered Useful,这是一篇很长的文章,但它使主题更加清晰,因为它非常详细地解释了继承在 C++ 中的工作原理(数字链接不起作用,但它们位于页面底部)。

      【讨论】:

        【解决方案3】:
        1. 如果对象 B 继承自 A,则 B 的内存表示如下:

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

          如果你有 B* b = new B(); (A)b->f() 然后:

          • 如果 f 被声明为虚函数,则调用 B 的实现,因为 b 是 B 类型
          • 如果 f 未声明为虚函数,则在调用时将不会在他的 vtable 中查找正确的实现,并且将调用 A 的实现。
        2. 每个对象都有自己的 vtable(不要想当然,因为我必须研究它

        3. 查看this 处理多重继承时的 vtable 布局示例

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

        【讨论】:

        • 编译器会尽可能重用指向基类子对象的虚拟表(vptr)的指针,除非必要,否则避免额外的vptr。
        • (A)b 不是指针转换,您正在构造另一个对象
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-07-21
        • 2013-11-19
        • 2016-05-20
        • 1970-01-01
        相关资源
        最近更新 更多