c++ - 了解多重继承中的虚拟表

标签 c++

我有一个实现两个抽象类的类,如下所示。没有虚拟继承。没有数据成员。

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变量:

  • obj + 0 处有一个指向虚拟表的指针。
  • 任何成员数据字段都将在 obj + 8 (sizeof(void*))处继续。
  • 虚拟表包含一条记录,该记录指向 vtbl + 0 上的void fcn(int)
  • 在该表中,还有一个指向 vtbl-8 (由typeinfo等使用)的类的dynamic_cast的指针,以及指向 vtbl-16 的“基础偏移”的指针。

  • 看到IFace1*类型的变量的任何函数都可以依赖于此为true。类似于IFace2*
  • 如果他们想调用虚拟函数void fcn(int),则查看 obj + 0 以获得vtable,然后查看 vtbl + 0 并调用在那里找到的地址。 this设置为 obj
  • 如果他们想访问一个成员字段(例如,自己访问该字段具有公共(public)访问权限,或者有一个内联访问器),他们只需在其地址 obj + xxx 上读/写该成员。
  • 如果他们想查看自己真正拥有的类型,则从对象的地址中减去 vtbl-16 的值,然后查看基础对象引用的vtable的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指针减少适当的数量
  • 调用real方法,该方法现在可以正常工作,而不管是谁调用了它。
  • 关于c++ - 了解多重继承中的虚拟表,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23504286/

    相关文章:

    c# - 从 WH_KEYBOARD_LL Hook 中向窗口发送消息

    c++ - 为什么允许引用指针的间接寻址?

    c++需要清晰的示例来显示 decltype(x) 和 decltype((x)) 之间的区别

    c++ - 有哪些具有快速附加和随机访问功能的可移植数据后端?

    c++ - 异步清理子进程

    c++ - Makefile 找不到包含路径

    c++ - 反转 C++ 元组

    c++ - ZMQ_DONTWAIT 标志不起作用?

    c++ - 64 位机器上 16 GB 数据的 STL 映射用法

    c++ - 读取 unicode 文件