虚函数并修改this指针
virtual function and modified this pointer
考虑以下代码
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
同样,我无法发表评论,所以必须在这里回答。
代码没问题!
D::f2
needs access to data from B1
then how is D::f2
able to access it?
只需写入D::f2
,B1::int_in_b1
即可访问int值。
你的情况实际上太简单了:编译器可以知道你有一个指向D
对象的指针,所以它可以从右边执行查找table,传递未修改的this
指向 f2()
实现的指针。
有趣的情况是,当您有一个指向 B2
:
的指针时
B2* myD = new D();
myD->f2();
现在我们从调整后的基指针开始,需要找到整个对象的this
指针。实现这一点的一种方法是在函数指针旁边存储一个偏移量,该函数指针用于从用于访问 vtable.[=21 的 B2
指针生成有效的 this
指针=]
因此,在您的情况下,代码可能会像这样隐式编译
D* myD = new D();
((B2*)myD)->f2();
调整指针两次(一旦从 D*
导出 B2*
,然后使用 vtable 的偏移量进行逆运算)。不过,您的编译器可能足够聪明,可以避免这种情况。
无论如何,这都在实施领域之内。您的编译器可以任何事情,只要它表现标准指定的方式。
在您的示例中,当调用 d->f2() 时,编译器知道 d 是指向 class D 的指针。要调用 f2(),它会将 d 的指针调整为 "this" 的 B2,然后将其传递给虚拟 f2(),如您所述。现在,在 D::f2() 内部,编译器知道这是 D::f2() 并且它知道 D 如何从 B2 继承,因此它将 B2 的 "this" 固定为 "this" 的 D 在函数的最开始,所以当你的代码执行时,它会看到 "this" 是 D 的。因此它可以访问 D::f2() 内部的 D 的任何成员.
如果你有
B2* b = d;
b->f2();
当调用b->f2()时,传递给f2()的指针是B2的"this"。在D::f2()中,传递的指针固定指向D的this。
首先,您描述为 "modifying a this
pointer" 的效果是某些特定编译器的实现细节。没有具体要求编译器像您描述的那样修改指针。
也没有要求对象必须有 vtables,更不用说它们像你描述的那样布局了。实际要求是在运行次调用虚函数的正确重载,能够正确访问数据成员和调用成员函数。现在,在实践中,编译器倾向于使用 vtables,但这是一个实现细节,因为从各种措施来看,替代方案的效率较低。
也就是说,下面的讨论将假设每个 class 具有虚函数的对象都有一个 vtable。查看您的示例,这是做什么的?
D *d = new D();
d->f2();
首先是编译器知道d
是指向D
的指针,并且知道D
有一个名为f2()
的函数。它还会知道 f2()
是从 B2
继承的虚函数(这是不可能调用 class 成员函数的原因之一,除非编译器可以看到完整的 class 定义).
在这种情况下,我们知道 d
和 D
是什么,所以我们知道应该调用 D::f2()
,this
指针的值等于 d
。编译器有相同的信息(它知道 d
是一个 D *
)所以它只是这样做。现在,好吧,它可能会或可能不会在 vtable 中查找 D::f2()
,但到此为止。
像cmaster所说的更有趣的例子是
B2* myD = new D();
myD->f2();
在这种情况下,myD
是指向 B2
的指针。编译器知道 B2
有一个名为 f2()
的虚函数,因此知道它必须调用正确的重载。
问题是,在语句 myD->f2()
中,编译器可能不知道 myD
实际上指向一个 D
(例如对象的构造和对象的调用成员函数可能在不同的函数中,在不同的编译单元中)。但是,它确实知道 B2
有一个名为 f2()
的虚函数,这是正确调用实际重载版本所必需的。
这意味着编译器需要两位信息。首先,它需要识别要调用的实际函数 (D::f2()
) 的信息。第二位信息是对 myD
的一些调整,以使 D::f2()
的调用正常工作。这第二位信息本质上是从 myD
.
生成(您所说的)"modified this
pointer" 所需的信息
如果编译器在 vtables 的帮助下完成所有这些工作,它可能会在 B2
的 vtable 中包含这两个信息位。所以(假设第二位信息是一个偏移量)编译器转
myD->f2();
变成类似
的东西
(myD + myD->vtable->offset_for_f2)->(myD->vtable->entry_for_f2)();
(myD + myD->vtable->offset_for_f2)
部分本质上就是您描述为 "the modified this
pointer" 的内容,D::f2()
将在调用时看到。 (myD->vtable->entry_for_f2)
部分本质上是 D::f2()
的地址(比如成员函数的地址)。
下一个要问的问题是编译器如何填充 vtable?简短的回答是它在构造对象时这样做。
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
的内存变成D
的过程很重要。首先,它调用基础 classes(B2
和 B2
)的构造函数,然后构造或初始化 D
s 成员,然后它调用 D
的构造函数(C++ 标准实际上非常详细地描述了事件的顺序)。另一件事是编译器进行簿记以确保我们实际上从进程中获得有效的 D
。其中一部分是填充 vtable。
现在,由于编译器对 class D
的定义具有完整的可见性(即基 classes 及其成员等的完整定义),它具有所有填充 vtable 所需的信息。换句话说,它具有为 myD->vtable->offset_for_f2
和 myD->vtable->entry_for_f2
提供合理值所需的所有信息
在多重继承的情况下,假设每个基有一个 vtable class,编译器拥有以类似方式填充所有 vtable 所需的所有信息。换句话说,编译器知道它如何在内存中布置对象,包括它们的 vtable,并适当地使用这些知识。
但是,话又说回来,它可能不会。正如我所说,vtables 是一种常用于编译器 implement/support 虚函数分派的技术。还有其他方法。
考虑以下代码
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
同样,我无法发表评论,所以必须在这里回答。
代码没问题!
D::f2
needs access to data fromB1
then how is
D::f2
able to access it?
只需写入D::f2
,B1::int_in_b1
即可访问int值。
你的情况实际上太简单了:编译器可以知道你有一个指向D
对象的指针,所以它可以从右边执行查找table,传递未修改的this
指向 f2()
实现的指针。
有趣的情况是,当您有一个指向 B2
:
B2* myD = new D();
myD->f2();
现在我们从调整后的基指针开始,需要找到整个对象的this
指针。实现这一点的一种方法是在函数指针旁边存储一个偏移量,该函数指针用于从用于访问 vtable.[=21 的 B2
指针生成有效的 this
指针=]
因此,在您的情况下,代码可能会像这样隐式编译
D* myD = new D();
((B2*)myD)->f2();
调整指针两次(一旦从 D*
导出 B2*
,然后使用 vtable 的偏移量进行逆运算)。不过,您的编译器可能足够聪明,可以避免这种情况。
无论如何,这都在实施领域之内。您的编译器可以任何事情,只要它表现标准指定的方式。
在您的示例中,当调用 d->f2() 时,编译器知道 d 是指向 class D 的指针。要调用 f2(),它会将 d 的指针调整为 "this" 的 B2,然后将其传递给虚拟 f2(),如您所述。现在,在 D::f2() 内部,编译器知道这是 D::f2() 并且它知道 D 如何从 B2 继承,因此它将 B2 的 "this" 固定为 "this" 的 D 在函数的最开始,所以当你的代码执行时,它会看到 "this" 是 D 的。因此它可以访问 D::f2() 内部的 D 的任何成员. 如果你有
B2* b = d;
b->f2();
当调用b->f2()时,传递给f2()的指针是B2的"this"。在D::f2()中,传递的指针固定指向D的this。
首先,您描述为 "modifying a this
pointer" 的效果是某些特定编译器的实现细节。没有具体要求编译器像您描述的那样修改指针。
也没有要求对象必须有 vtables,更不用说它们像你描述的那样布局了。实际要求是在运行次调用虚函数的正确重载,能够正确访问数据成员和调用成员函数。现在,在实践中,编译器倾向于使用 vtables,但这是一个实现细节,因为从各种措施来看,替代方案的效率较低。
也就是说,下面的讨论将假设每个 class 具有虚函数的对象都有一个 vtable。查看您的示例,这是做什么的?
D *d = new D();
d->f2();
首先是编译器知道d
是指向D
的指针,并且知道D
有一个名为f2()
的函数。它还会知道 f2()
是从 B2
继承的虚函数(这是不可能调用 class 成员函数的原因之一,除非编译器可以看到完整的 class 定义).
在这种情况下,我们知道 d
和 D
是什么,所以我们知道应该调用 D::f2()
,this
指针的值等于 d
。编译器有相同的信息(它知道 d
是一个 D *
)所以它只是这样做。现在,好吧,它可能会或可能不会在 vtable 中查找 D::f2()
,但到此为止。
像cmaster所说的更有趣的例子是
B2* myD = new D();
myD->f2();
在这种情况下,myD
是指向 B2
的指针。编译器知道 B2
有一个名为 f2()
的虚函数,因此知道它必须调用正确的重载。
问题是,在语句 myD->f2()
中,编译器可能不知道 myD
实际上指向一个 D
(例如对象的构造和对象的调用成员函数可能在不同的函数中,在不同的编译单元中)。但是,它确实知道 B2
有一个名为 f2()
的虚函数,这是正确调用实际重载版本所必需的。
这意味着编译器需要两位信息。首先,它需要识别要调用的实际函数 (D::f2()
) 的信息。第二位信息是对 myD
的一些调整,以使 D::f2()
的调用正常工作。这第二位信息本质上是从 myD
.
this
pointer" 所需的信息
如果编译器在 vtables 的帮助下完成所有这些工作,它可能会在 B2
的 vtable 中包含这两个信息位。所以(假设第二位信息是一个偏移量)编译器转
myD->f2();
变成类似
的东西(myD + myD->vtable->offset_for_f2)->(myD->vtable->entry_for_f2)();
(myD + myD->vtable->offset_for_f2)
部分本质上就是您描述为 "the modified this
pointer" 的内容,D::f2()
将在调用时看到。 (myD->vtable->entry_for_f2)
部分本质上是 D::f2()
的地址(比如成员函数的地址)。
下一个要问的问题是编译器如何填充 vtable?简短的回答是它在构造对象时这样做。
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
的内存变成D
的过程很重要。首先,它调用基础 classes(B2
和 B2
)的构造函数,然后构造或初始化 D
s 成员,然后它调用 D
的构造函数(C++ 标准实际上非常详细地描述了事件的顺序)。另一件事是编译器进行簿记以确保我们实际上从进程中获得有效的 D
。其中一部分是填充 vtable。
现在,由于编译器对 class D
的定义具有完整的可见性(即基 classes 及其成员等的完整定义),它具有所有填充 vtable 所需的信息。换句话说,它具有为 myD->vtable->offset_for_f2
和 myD->vtable->entry_for_f2
在多重继承的情况下,假设每个基有一个 vtable class,编译器拥有以类似方式填充所有 vtable 所需的所有信息。换句话说,编译器知道它如何在内存中布置对象,包括它们的 vtable,并适当地使用这些知识。
但是,话又说回来,它可能不会。正如我所说,vtables 是一种常用于编译器 implement/support 虚函数分派的技术。还有其他方法。