多重继承的虚函数表

7

我正在阅读这篇文章 "虚函数表"

在上述文章中的示例:

class B1 {
public:
  void f0() {}
  virtual void f1() {}
  int int_in_b1;
};

class B2 {
public:
  virtual void f2() {}
  int int_in_b2;
};

class D : public B1, public B2 {
public:
  void d() {}
  void f2() {}  // override B2::f2()
  int int_in_d;
};

B2 *b2 = new B2();
D  *d  = new D();

在这篇文章中,作者介绍了对象d的内存布局如下:
          d:
D* d-->      +0: pointer to virtual method table of D (for B1)
             +4: value of int_in_b1
B2* b2-->    +8: pointer to virtual method table of D (for B2)
             +12: value of int_in_b2
             +16: value of int_in_d

Total size: 20 Bytes.

virtual method table of D (for B1):
  +0: B1::f1()  // B1::f1() is not overridden

virtual method table of D (for B2):
  +0: D::f2()   // B2::f2() is overridden by D::f2()

这个问题涉及到 d->f2()。调用 d->f2() 时将一个 B2 指针作为 this 指针传递,因此我们需要进行类似以下的操作:
(*(*(d[+8]/*pointer to virtual method table of D (for B2)*/)[0]))(d+8) /* Call d->f2() */

为什么我们应该将B2指针作为this指针传递,而不是原始的D指针???实际上,我们正在调用D :: f2()。根据我的理解,在D :: f2()函数中,我们应该传递一个D指针作为this
___更新___
如果将一个B2指针作为this传递给D::f2(),那么如果我们想要访问B1类的成员怎么办?我相信B2指针(this)就像这样:
          d:
D* d-->      +0: pointer to virtual method table of D (for B1)
             +4: value of int_in_b1
B2* b2-->    +8: pointer to virtual method table of D (for B2)
             +12: value of int_in_b2
             +16: value of int_in_d

这个连续的内存布局已经有了一定的起始地址偏移量。例如,我们想在D :: f2()内部访问b1,我想在运行时,它会执行类似于:*(this+4)this指向与b2相同的地址),这将指向B中的b2吗?

2个回答

4

我们不能将 D 指针传递给覆盖 B2::f2() 的虚函数,因为所有覆盖相同虚函数的函数必须接受相同的内存布局。

由于 B2::f2() 函数期望将传递给它的对象的 this 指针作为 B2 的内存布局,即

b2:
  +0: pointer to virtual method table of B2
  +4: value of int_in_b2

覆盖函数D::f2()必须预期相同的布局。否则,这些函数将不再可互换。

为了说明可互换性的重要性,请考虑以下情景:

class B2 {
public:
  void test() { f2(); }
  virtual void f2() {}
  int int_in_b2;
};
...
B2 b2;
b2.test(); // Scenario 1
D d;
d.test(); // Scenario 2
B2::test()需要在两种情况下都调用f2(),但它没有其他信息来告诉它在进行这些调用时应如何调整this指针*。这就是为什么编译器传递修正后的指针,使得test()D::f2()B2::f2()的调用都能正常工作。

* 其他实现可能会传递此信息;然而,本文讨论的多重继承实现不这样做。


(1) "可互换"是什么意思,您能详细解释一下吗? (2) 如果将一个指向B2的指针作为this传递给D::f2(),如果我们想在D::f2()中访问B1类的成员,该怎么办?有关(2),请参见问题的更新。 - Fihop
非常感谢!请验证我。B2 b2; b2.test(),它肯定是将 B2 指针作为 this 传递给 B2::test(),因为 b2 是一个独立的对象。对于 D d; d.test(),编译器会将修复指针传递给 test(),该指针实际上指向 D 的子对象 B2,因为实际调用函数是 B2::test()。如果 thisD 中没有指向 B2,那么在函数 B2::test() 中访问 B2 成员时就会出现问题。这就是为什么我认为我们应该将修复指针作为 this 传递的原因。这个例子无法解释更新。还是要感谢。 - Fihop
对于 D d; d.test(),我同意我们可以像这样做 d.test(B2* b2)(意味着this指向D的子对象B2)。然而,在B2:test内部,b2->f2()应该执行D::f2。我是正确的吗?现在问题变成了传递给b2->f2()的是什么类型的this - Fihop
1
还要考虑void foo(const B2&b2){b2.f2();}的常见情况。调用类型为D的对象的foo()需要起作用,但是foo()无法传递指向类型为Dthis指针的可能性。因此,编译器必须编译假定D :: f2()的代码,假定隐藏的指针参数是指向B2类型的。但是,它可以假定隐藏的指针参数指向嵌入在D类型中的B2类型。因此,该函数仍然可以访问D'成员(但必须适当自动调整传入的this指针)。 - Michael Burr
1
@FihopZz "在B2:test内,b2->f2()应该执行D::f2" 你说得对!这正是为什么在D内传递给test()this指针必须是B2的原因,因为test()无法调整D::f2()的调用指针。因此,它传递了自己的this,无论如何被调用,都可以工作,因为B2::f2D::f2都期望通过this指针传递相同的布局。 - Sergey Kalinichenko

1
根据你的类层次结构,类型为B2的对象将具有以下内存占用。
+------------------------+
| pointer for B2 vtable  |
+------------------------+
| int_in_b2              |
+------------------------+

D类型的对象将具有以下内存占用。

+------------------------+
| pointer for B1 vtable  |
+------------------------+
| int_in_b1              |
+------------------------+
| pointer for B2 vtable  |
+------------------------+
| int_in_b2              |
+------------------------+
| int_in_d               |
+------------------------+

When you use:

D* d  = new D();
d->f2();

那个调用与以下代码相同:

B2* b  = new D();
b->f2();

f2()可以使用类型为B2D的指针进行调用。鉴于运行时必须能够正确地处理B2类型的指针,因此它必须能够使用B2的vtable中的适当函数指针正确地分派对D::f2()的调用。然而,当调用分派到D:f2()时,必须以某种方式正确偏移类型为B2的原始指针,以便在D::f2()中,this指向一个D,而不是B2

以下是您的示例代码,稍作修改以打印有用的指针值和成员数据,以帮助理解各个函数中this值的变化。

#include <iostream>

struct B1 
{
   void f0() {}
   virtual void f1() {}
   int int_in_b1;
};

struct B2 
{
   B2() : int_in_b2(20) {}
   void test_f2()
   {
      std::cout << "In B::test_f2(), B*: " << (void*)this << std::endl;
      this->f2();
   }

   virtual void f2()
   {
      std::cout
         << "In B::f2(), B*: " << (void*)this
         << ", int_in_b2: " << int_in_b2 << std::endl;
   }

   int int_in_b2;
};

struct D : B1, B2 
{
   D() : int_in_d(30) {}
   void d() {}
   void f2()
   {
      // ======================================================
      // If "this" is not adjusted properly to point to the D
      // object, accessing int_in_d will lead to undefined 
      // behavior.
      // ======================================================

      std::cout
         << "In D::f2(), D*: " << (void*)this
         << ", int_in_d: " << int_in_d << std::endl;
   }
   int int_in_d;
};

int main()
{
   std::cout << "sizeof(void*) : " << sizeof(void*) << std::endl;
   std::cout << "sizeof(int)   : " << sizeof(int) << std::endl;
   std::cout << "sizeof(B1)    : " << sizeof(B1) << std::endl;
   std::cout << "sizeof(B2)    : " << sizeof(B2) << std::endl;
   std::cout << "sizeof(D)     : " << sizeof(D) << std::endl << std::endl;

   B2 *b2 = new B2();
   D  *d  = new D();
   b2->test_f2();
   d->test_f2();
   return 0;
}

程序的输出:

sizeof(void*) : 8
sizeof(int)   : 4
sizeof(B1)    : 16
sizeof(B2)    : 16
sizeof(D)     : 32

In B::test_f2(), B*: 0x1f50010
In B::f2(), B*: 0x1f50010, int_in_b2: 20
In B::test_f2(), B*: 0x1f50040
In D::f2(), D*: 0x1f50030, int_in_d: 30

当实际用于调用test_f2()的对象为D时,this的值从test_f2()中的0x1f50040更改为D::f2()中的0x1f50030。这与B1、B2和D的大小相匹配。D对象的B2子对象的偏移量为16(0x10)。在将调用分派到D::f2()之前,B::test_f2()中B*的this值会改变0x10。我猜想,从D到B2的偏移量的值存储在B2的虚表中。否则,通用函数分派机制无法在将调用分派到正确的虚函数之前正确地更改this的值。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接