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();

在文章中,作者介绍了object的内存布局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指向thisD::f2() 函数的指针。

___更新____

如果传递一个B2指向thisD::f2() 的指针,如果我们想访问B1D::f2() 中的类成员怎么办?我相信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

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

4

2 回答 2

4

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

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

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()' 的调用f2可以与D::f2()和一起使用B2::f2()

*其他实现可能会很好地传递此信息;但是,文章中讨论的多重继承实现并没有做到这一点。

于 2015-06-10T20:59:41.297 回答
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               |
+------------------------+

当您使用:

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

该调用与以下内容相同:

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

f2()可以使用类型B2指针或类型指针调用D。鉴于运行时必须能够正确使用 类型的指针B2,它必须能够使用的 vtableD::f2()中的适当函数指针正确调度调用。B2但是,当调用被分派到D:f2()类型的原始指针时,必须以某种方式正确偏移,B2以便 in指向 a ,而不是 a 。D::f2()thisDB2

这是您的示例代码,稍作改动以打印有用的指针值和成员数据,以帮助理解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时, 的值this0x1f50040intest_f2()变为0x1f50030in D::f2()。这与 sizeof B1B2D. B2对象的子对象的偏移量D16 (0x10)thisin B::test_f2(), a ,的值在调用被调度到 之前B*被改变。0x10D::f2()

我猜测从Dto的偏移值B2存储在B2的 vtable 中。this否则,在将调用分派到正确的虚函数之前,通用函数分派机制无法正确更改 的值。

于 2015-06-11T03:23:54.240 回答