C++中的多个虚拟表格和虚拟观众

Multiple Vtables ad VPointers in C++

本文关键字:表格 虚拟观 虚拟 C++      更新时间:2023-10-16

我一直在阅读 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:父母和孩子会有3个vtable和3个vpoint吗?

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

是的,明确的答案取决于编译器。 甚至不能保证虚拟调度将在 vtables 中实现。

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

至少在Linux上,在实践中了解更多关于vtables的一种方法是使用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->PrintChild()p1Parent*。 在这里,调用将通过您在上面看到的第一个 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特有的。 其他系统可能会做出不同的选择。