我一直在阅读vtable和指针,但我还有一些问题。例如:
#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个vpointers吗?
问题2:p1将如何知道使用哪个vpointer?我听说这取决于编译器,但我只是希望有人澄清。
答案 0 :(得分:1)
是的,确定的答案取决于编译器。甚至不能保证用vtable实现虚拟调度。
编译器通常会遵循特定平台的ABI。在许多系统中,GCC实现了为IA-64发明的特定ABI,但随后被移植到其他系统。这在网上很容易获得,有GCC网站的链接。
在Linux上至少可以看到更多关于vtables的一种方法是使用gdb
,使用-g
编译一个小示例程序,并使用info vtbl
来探索vtable 。然而,由于涉及虚拟析构函数的调试信息的GCC错误,目前这有点棘手;所以只要确保总有除析构函数之外的其他方法。
我编译了您的程序,并在gdb
初始化后停在p1
。然后:
(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()>
在这里,您可以看到Parent
和Child
实际上只有两个 vtable,而不是三个。这是因为在这个ABI中,通过扩展父类'vtable来实现单继承;在这种情况下,A
的扩展名也会以同样的方式处理。
关于p1
如何知道使用哪个vtable:它取决于用于进行调用的实际类型。
在代码中,p1->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所特有的。其他系统可能会做出不同的选择。