虚函数并修改了这个指针

时间:2015-11-19 18:34:26

标签: c++ multiple-inheritance vtable abi virtual-method

考虑以下代码

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() {int temp=int_in_b1;}  // override B2::f2()
  int int_in_d;
};

以及对象d的以下内存布局:

d:
  +0: pointer to virtual method table of D (for B1)
  +4: value of int_in_b1
  +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  *d  = new D();

d->f2();

调用d->f2();时,D::f2需要访问来自B1的数据,但修改了此指针

(*(*(d[+8]/*pointer to virtual method table of D (for B2)*/)[0]))(d+8) /* Call d->f2() */

传递给D::f2,那么D::f2如何才能访问它?

https://en.wikipedia.org/wiki/Virtual_method_table#Multiple_inheritance_and_thunks

获取(并修改)代码

3 个答案:

答案 0 :(得分:2)

你的情况实际上太简单了:编译器可以知道你有一个指向D对象的指针,所以它可以从右表执行查找,将未修改的this指针传递给f2()实施。

有趣的情况是,当你有一个指向B2的指针:

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

现在我们从调整后的基指针开始,需要找到整个对象的this指针。实现这一目标的一种方法是在函数指针旁边存储一个偏移量,用于从用于访问vtable的this指针生成有效的B2指针。

因此,在您的情况下,代码可能会隐式编译为

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

将指针调整两次(一次从B2*导出D*,然后使用vtable的偏移量进行反演)。但是,您的编译器可能足够聪明,可以避免这种情况。

无论如何,这完全属于实施领域。只要行为符合标准指定的方式,您的编译器就可以执行任何

答案 1 :(得分:0)

我再也不能评论所以必须在这里回答。

代码没问题!

  

D::f2需要访问B1

中的数据      

那么D::f2如何才能访问它?

只需写入D::f2B1::int_in_b1即可访问int值。

答案 2 :(得分:0)

首先,您所描述的效果为"修改this指针"是一些特定编译器的实现细节。没有特别要求编译器像你描述的那样修改指针。

也没有要求对象有vtable,更不用说它们像你描述的那样布局了。实际要求是在运行时调用虚函数的正确重载,并且能够正确访问数据成员和调用成员函数。现在,在实践中,编译器倾向于使用vtable,但这是一个实现细节,因为各种措施的替代方案效率较低。

现在,这就是说,下面的讨论将假设每个具有虚函数的类都有一个vtable。看看你的例子,这是做什么的?

D  *d  = new D();

d->f2();

第一件事是编译器知道d是指向D的指针,并且知道D有一个名为f2()的函数。它还将知道f2()是从B2继承的虚函数(这是除非编译器具有完整类定义的可见性,否则无法调用类成员函数的一个原因)。 / p>

在这种情况下,我们知道dD是什么,因此我们知道应该调用D::f2()this指针的值等于{{1} }}。编译器具有相同的信息(它知道dd)所以它就是这样做的。现在,好吧,它可能会或可能不会在vtable中查找D *,但这就是结束。

像cmaster所说,更有趣的例子是

D::f2()

在这种情况下,B2* myD = new D(); myD->f2(); 是指向myD的指针。编译器知道B2有一个名为B2的虚函数,因此知道它必须调用正确的重载。

问题是,在语句f2()中,编译器可能不知道myD->f2()实际指向myD(例如对象的构造和成员函数的调用)可能在不同的函数中,在不同的编译单元中)。但是,它确实知道D有一个名为B2的虚函数,需要正确调用实际的重载版本。

这意味着编译器需要两位信息。首先,它需要识别要调用的实际函数(f2())的信息。第二位信息将是D::f2()的一些调整,以使myD的调用正常工作。第二位信息基本上是生成(你正在调用的)"修改后的D::f2()指针"来自this

如果编译器在vtables的帮助下完成所有这些操作,它可能在myD的vtable中包含两个信息位。所以(假设第二位信息是偏移量)编译器转动

B2

类似

myD->f2();

部分(myD + myD->vtable->offset_for_f2)->(myD->vtable->entry_for_f2)(); 基本上就是你所描述的"修改后的(myD + myD->vtable->offset_for_f2)指针"调用时this会看到哪个。部分D::f2()本质上是(myD->vtable->entry_for_f2)的地址(比如成员函数的地址)。

接下来要问的问题是编译器如何填充vtable?简短的回答是它在构造对象时执行此操作。

D::f2()

新表达式(B2* myD = new D(); )基本上扩展为

new D()

将指向void *temp = ::operator new(sizeof (D)); // assuming class does not supply its own operator new // construct a `D` in the memory pointed to by temp temp = (D *)myD; // the compiler knows we're creating a D, so doesn't use offsets or anything funky here 的内存转换为temp的过程非常重要。首先,它调用基类的构造函数(DB2),然后构造或初始化B2成员,然后调用D的构造函数( C ++标准实际上以精致的细节描述了事件的顺序。另一件事是编译器进行簿记以确保我们实际从进程中获得有效的D。其中一部分是填充vtable。

现在,由于编译器完全可以看到类D的定义(即基类的完整定义,其成员等),因此它具有填充vtable所需的所有信息。换句话说,它拥有为DmyD->vtable->offset_for_f2提供合理价值所需的所有信息

在多重继承的情况下,假设每个基类有一个vtable,编译器具有以类似方式填充所有vtable所需的所有信息。换句话说,编译器知道它如何在内存中放置对象,包括它们的vtable,并适当地使用这些知识。

但是,然而,它可能不会。正如我所说,vtables是编译器中常用的一种技术,用于实现/支持虚函数调度。还有其他方法。