用于多重继承的虚拟方法表
virtual method table for multiple-inheritance
我正在阅读这篇文章"Virtual method table"
以上文章中的例子:
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
指针作为this
传递给D::f2()函数。
___update____
如果将B2
指针作为this
传递给D::f2(),如果我们想在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
不能将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()
。
*其他实现可以很好地传递此信息;然而,本文中讨论的多重继承实现并没有做到这一点。
给定您的类层次结构,类型为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
的指针,它必须能够使用B2
的虚函数表中适当的函数指针正确地调度对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
。匹配sizeof B1
, B2
和D
。D
对象的B2
子对象的偏移量为16 (0x10)
。B::test_f2()
(B*
)中的this
的值被0x10
修改后才被调度到D::f2()
。
我猜从D
到B2
的偏移量的值存储在B2
的虚表中。否则,在将调用调度到正确的虚函数之前,泛型函数调度机制无法正确地更改this
的值。
- 用常见虚拟函数实现的任意组合来实现派生类的正确方法是什么
- 在模板基类中为继承类中的可选重写生成虚拟方法
- 跨 DLL 边界访问虚拟方法是否安全/可能?
- 是否可以使用基类非虚拟方法中的派生类虚拟方法?
- 如何编写 operator= 用于使用虚拟方法与非平凡成员的匿名联合
- 让编译器告诉什么确切的纯虚拟方法使结构抽象?
- 使用模板而不是虚拟方法的管道模式
- 派生类调用父类的方法,该方法调用重写的虚拟方法调用错误的方法
- 从纯虚拟类 (A) 派生的指针无法访问来自纯类 (B) 的重载方法
- 为什么调用没有正文的纯虚拟方法不会导致链接器错误?
- 出于什么目的,非虚拟方法将与C++一起使用?
- 为什么使用存储在虚拟方法表中的地址调用虚拟函数的函数会返回垃圾?
- 如何重写继承的嵌套类中存在的虚拟方法
- 私有虚拟方法有什么用?
- 派生类中纯虚拟基方法的专业化
- 基类可以声明虚拟方法但不定义它吗?仍然在派生类中定义
- googletest:测试基类具有纯虚拟方法的派生类时的核心转储
- 确保模拟的 GTest 方法覆盖虚拟方法
- 使用回调函数从构造函数调用虚拟/派生方法的替代方法?
- 如何调用孩子的方法:虚拟关键字不起作用