我有一个实现两个抽象类的类,如下所示。没有虚拟继承。没有数据成员。
class IFace1 {
public:
virtual void fcn(int abc) = 0;
};
class IFace2 {
public:
virtual void fcn1(int abc) = 0;
};
class RealClass: public IFace1, public IFace2 {
public:
void fcn(int a) {
}
void fcn1(int a) {
}
};
而且我发现RealClass的vtable和对象内存布局如下所示。
Vtable for RealClass
RealClass::_ZTV9RealClass: 7u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI9RealClass)
16 (int (*)(...))RealClass::fcn
24 (int (*)(...))RealClass::fcn1
32 (int (*)(...))-8
40 (int (*)(...))(& _ZTI9RealClass)
48 (int (*)(...))RealClass::_ZThn8_N9RealClass4fcn1Ei
Class RealClass
size=16 align=8
base size=16 base align=8
RealClass (0x2af836d010e0) 0
vptr=((& RealClass::_ZTV9RealClass) + 16u)
IFace1 (0x2af836cfa5a0) 0 nearly-empty
primary-for RealClass (0x2af836d010e0)
IFace2 (0x2af836cfa600) 8 nearly-empty
vptr=((& RealClass::_ZTV9RealClass) + 48u)
我对此感到困惑。什么是RealClass::__ ZThn8_N9RealClass4fcn1Ei?为什么IFace2的vptr指向这一点?当我从IFace2 *调用fcn1时会发生什么?程序如何在RealClass的Vtable中找到RealClass::fcn1?我猜想它需要使用IFace2 vptr,但不清楚具体如何。
最佳答案
警告:下面的大多数内容当然都是实现和平台相关且已简化。我将按照您在示例中看到的实现方式进行操作-可能是GCC(64位)。
首先,虚拟类实例的契约(Contract)是什么?例如。如果您有一个IFace1* obj
变量:
sizeof(void*)
)处继续。 void fcn(int)
。 typeinfo
等使用)的类的dynamic_cast
的指针,以及指向 vtbl-16 的“基础偏移”的指针。 看到
IFace1*
类型的变量的任何函数都可以依赖于此为true。类似于IFace2*
。void fcn(int)
,则查看 obj + 0 以获得vtable,然后查看 vtbl + 0 并调用在那里找到的地址。 this
设置为 obj 。 typeinfo
指针。 现在,对于具有多重继承的类,编译器如何满足这些要求?
1)首先,它需要自行生成结构。虚拟表指针必须位于 obj + 0 处,就可以了。桌子看起来如何?好吧,到基数的偏移量是0,显然,
typeinfo
数据和指向它的指针很容易生成,然后是第一个虚函数和第二个虚函数,没什么特别的。知道RealClass
的定义的任何人都可以进行相同的计算,因此他们知道在vtable等中找到功能的位置。2)然后可以使
RealClass
作为IFace1
传递。因此,它需要在对象的某处有一个指向IFace1
格式的虚拟表的指针,然后该虚拟表必须具有void fcn(int)
的一条记录。编译器很聪明,并且看到它可以重复使用它所生成的第一个虚拟表,因为它符合这些要求。如果有任何成员字段,它们将存储在指向虚拟表的第一个指针之后,因此甚至可以像派生类是基类一样简单地访问它们。到现在为止还挺好。
3)最后,如何处理对象,以便其他人可以将其用作
IFace2
?已经创建的一个vtable不能再使用,因为IFace2
需要其void fcn1(int)
位于 vtbl + 0 。因此,将创建另一个虚拟表,您将在转储中的第一个虚拟表之后立即看到该虚拟表,并将指向该虚拟表的指针存储在下一个可用位置的
RealClass
中。第二个表需要将基本偏移量设置为-8,因为实际对象从偏移量-8开始。它仅包含指向该IFace2
虚拟函数void fcn1(int)
的指针。然后,对象中的虚拟指针(偏移量为 obj + 8 )之后是
IFace2
的任何成员数据字段,因此,当给定指向此接口(interface)的指针时,任何继承或内联函数都可以再次工作。好的,现在有人可以从
fcn1()
调用IFace2
吗?那non-virtual thunk to RealClass::fcn1(int)
是什么?如果您将
RealClass*
指针传递给采用IFace2*
的陌生人函数,则编译器将发出代码以将您的指针增加8(或较大的sizeof(void*) + sizeof(IFace1)
),以便该函数获得以IFace2
的虚拟表指针开头的指针,然后是其成员字段-就像我之前概述的契约(Contract)中所约定的那样。当该函数要调用
void IFace2::fcn1(int)
时,它将查看虚拟表,转到该特定函数的记录(第一个也是唯一一个),并对其进行调用,并将this
设置为要作为指向IFace2
的指针的地址。这就产生了一个问题:如果有人在
RealClass
指针上调用在RealClass
中实现的此方法,则this
指向RealClass
的基数。与IFace1
相同。但是,如果有人指向IFace2
接口(interface)的指针调用了它,则this
会将8(或许多)字节指向对象!因此,编译器将需要多次生成函数以适应此要求,否则编译器将无法正确访问成员字段和其他方法,因为根据调用该方法的人而有所不同。
编译器没有创建真正的代码两次,而是通过创建隐藏的隐式小 thunk 函数来优化此代码,该函数只是
this
指针减少适当的数量关于c++ - 了解多重继承中的虚拟表,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23504286/