考虑以下代码
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
获取(并修改)代码答案 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::f2
,B1::int_in_b1
即可访问int值。
答案 2 :(得分:0)
首先,您所描述的效果为"修改this
指针"是一些特定编译器的实现细节。没有特别要求编译器像你描述的那样修改指针。
也没有要求对象有vtable,更不用说它们像你描述的那样布局了。实际要求是在运行时调用虚函数的正确重载,并且能够正确访问数据成员和调用成员函数。现在,在实践中,编译器倾向于使用vtable,但这是一个实现细节,因为各种措施的替代方案效率较低。
现在,这就是说,下面的讨论将假设每个具有虚函数的类都有一个vtable。看看你的例子,这是做什么的?
D *d = new D();
d->f2();
第一件事是编译器知道d
是指向D
的指针,并且知道D
有一个名为f2()
的函数。它还将知道f2()
是从B2
继承的虚函数(这是除非编译器具有完整类定义的可见性,否则无法调用类成员函数的一个原因)。 / p>
在这种情况下,我们知道d
和D
是什么,因此我们知道应该调用D::f2()
,this
指针的值等于{{1} }}。编译器具有相同的信息(它知道d
是d
)所以它就是这样做的。现在,好吧,它可能会或可能不会在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
的过程非常重要。首先,它调用基类的构造函数(D
和B2
),然后构造或初始化B2
成员,然后调用D
的构造函数( C ++标准实际上以精致的细节描述了事件的顺序。另一件事是编译器进行簿记以确保我们实际从进程中获得有效的D
。其中一部分是填充vtable。
现在,由于编译器完全可以看到类D
的定义(即基类的完整定义,其成员等),因此它具有填充vtable所需的所有信息。换句话说,它拥有为D
和myD->vtable->offset_for_f2
提供合理价值所需的所有信息
在多重继承的情况下,假设每个基类有一个vtable,编译器具有以类似方式填充所有vtable所需的所有信息。换句话说,编译器知道它如何在内存中放置对象,包括它们的vtable,并适当地使用这些知识。
但是,然而,它可能不会。正如我所说,vtables是编译器中常用的一种技术,用于实现/支持虚函数调度。还有其他方法。