【问题标题】:Multiple Vtables ad VPointers in C++多个表在 C++ 中添加指针
【发布时间】:2015-06-17 02:07:05
【问题描述】:

我一直在阅读有关 vtables 和指针的内容,但仍有一些问题。例如:

#include <iostream>

using namespace std;

class A
{
 public:
  virtual void PrintA()=0;  //1 vtable and 1 vpointer
};

class B
{
 public:
  virtual void PrintB()=0; //1 vtable and 1 vpointer
};

class Parent: public A, public B
{
 public:
  void PrintA();
  void PrintB();                // 3 vtables and 3 vpointers, right?
  virtual void PrintChild()=0;
};


void Parent::PrintA()
{
 cout<<"A";
}

void Parent::PrintB()
{
 cout<<"B";
}


class Child: public Parent
{
 public:
   void PrintChild(); //3 vtables and 3 vpointers
};

void Child::PrintChild()
{
 cout<<"Child";
}

int main()
{

  Parent* p1= new Child();
  p1->PrintChild();
  delete p1;
  return 0;
}

问题 1:Parent 和 Child 会有 3 个 vtables 和 3 个 vpointer 吗?

问题 2:p1 如何知道要使用哪个 vpointer?我听说这取决于编译器,但我只想有人澄清一下。

【问题讨论】:

    标签: c++ vtable


    【解决方案1】:

    是的,最终答案取决于编译器。甚至无法保证虚拟调度将使用 vtables 实现。

    编译器通常会遵循特定平台的 ABI。在许多系统上,GCC 实现了为 IA-64 发明的特定 ABI,但随后被移植到其他系统。这在网上很容易获得,有来自 GCC 网站的链接。

    在实践中了解更多关于 vtables 的方法,至少在 Linux 上,是使用 gdb,使用 -g 编译一个小示例程序,然后使用 info vtbl 来探索 vtable。但是,由于涉及虚拟析构函数的调试信息的 GCC 错误,目前这有点棘手;因此,请确保始终使用析构函数以外的方法。

    我编译了你的程序并在p1 初始化后停在了gdb。那么:

    (gdb) info vtbl p1
    vtable for 'Parent' @ 0x400a10 (subobject @ 0x602010):
    [0]: 0x400806 <Parent::PrintA()>
    [1]: 0x400810 <Parent::PrintB()>
    [2]: 0x400824 <Child::PrintChild()>
    
    vtable for 'B' @ 0x400a38 (subobject @ 0x602018):
    [0]: 0x40081a <non-virtual thunk to Parent::PrintB()>
    

    您可以看到ParentChild 实际上只有两个 vtable,而不是三个。这是因为在这个ABI中,单继承是通过扩展父类的vtable来实现的;在这种情况下,A 的扩展名也会以同样的方式处理。

    至于p1 如何知道要使用哪个 vtable:这取决于用于进行调用的实际类型。

    在代码中,p1-&gt;PrintChild() 被调用,p1 是一个Parent*。在这里,将通过您在上面看到的第一个 vtable 进行调用——因为没有其他意义,因为 PrintChild 没有在 B 中声明。在此 ABI 中,vtable 存储在对象的第一个槽中:

    (gdb) p *(void **)p1
    $1 = (void *) 0x400a10 <vtable for Child+16>
    

    现在,如果您将代码更改为将 p1 转换为 B*,则会发生两件事。首先,指针的原始位会改变,因为新指针将指向完整对象的子对象。其次,该子对象的 vtable 插槽将指向上面提到的第二个 vtable。在这种情况下,有时会对子对象应用特殊偏移以再次找到完整对象。当使用virtual 继承时,也有一些特殊的调整(这会使对象布局有点复杂,因为有问题的超类在布局中只出现一次)。

    您可以像这样看到这些变化:

    (gdb) p (B*)p1
    $2 = (B *) 0x602018
    (gdb) p *(void**)(B*)p1
    $3 = (void *) 0x400a38 <vtable for Child+56>
    

    这几乎都是 Linux 上常用的 ABI 特有的。其他系统可能会做出不同的选择。

    【讨论】:

      猜你喜欢
      • 2021-09-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-06-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多