C++ 多重虚拟继承中的虚拟表和内存布局
考虑以下层次结构:C++ 多重虚拟继承中的虚拟表和内存布局,c++,multiple-inheritance,vtable,virtual-inheritance,memory-layout,C++,Multiple Inheritance,Vtable,Virtual Inheritance,Memory Layout,考虑以下层次结构: struct A { int a; A() { f(0); } A(int i) { f(i); } virtual void f(int i) { cout << i; } }; struct B1 : virtual A { int b1; B1(int i) : A(i) { f(i); } virtual void f(int i) { cout << i+10; } }; struct B2 : v
struct A {
int a;
A() { f(0); }
A(int i) { f(i); }
virtual void f(int i) { cout << i; }
};
struct B1 : virtual A {
int b1;
B1(int i) : A(i) { f(i); }
virtual void f(int i) { cout << i+10; }
};
struct B2 : virtual A {
int b2;
B2(int i) : A(i) { f(i); }
virtual void f(int i) { cout << i+20; }
};
struct C : B1, virtual B2 {
int c;
C() : B1(6),B2(3),A(1){}
virtual void f(int i) { cout << i+30; }
};
其中,AptrOfBx
是指向Bx
包含的实例的指针(因为继承是虚拟的)。
对吗?vptr1
指向哪些功能?vptr2
指向哪些功能
给定以下代码
C* c = new C();
dynamic_cast<B1*>(c)->f(3);
static_cast<B2*>(c)->f(3);
reinterpret_cast<B2*>(c)->f(3);
C*C=newc();
动态铸造(c)->f(3);
静态铸造(c)->f(3);
重新解释铸造(c)->f(3);
为什么要调用f
print33
虚拟基地与普通基地大不相同。请记住,“virtual”意味着“在运行时确定”——因此,整个基本子对象必须在运行时确定
假设您正在获得一个B&x
引用,您的任务是查找a::a
成员。如果继承是真实的,那么B
有一个超类a
,因此通过x
查看的B
-对象有一个a
-子对象,您可以在其中找到成员a::a
。如果x
的最派生对象有多个A
类型的基,则只能看到作为B
子对象的特定副本
但是如果继承是虚拟的,那么这些都没有意义。我们不知道我们需要哪个子对象,这些信息在编译时根本不存在。我们可以处理实际的B
-对象,如by;B&x=y
,或使用类似cz的C
-对象;B&x=z代码>,或者是从A
中多次衍生出来的完全不同的东西。知道的唯一方法是在运行时找到实际的基A
这可以通过一个更高级别的运行时间接寻址来实现。(请注意,与非虚拟函数相比,虚拟函数是如何通过一个额外的运行时间接寻址级别来实现的。)一种解决方案是存储指向实际基子对象的指针,而不是使用指向vtable或base子对象的指针。这有时被称为“扑通”或“蹦床”
因此,实际对象cz代码>可能如下所示。内存中的实际顺序由编译器决定,并不重要,我已经抑制了vtables
+-+------++-+------++-----++-----+
|T| B1 ||T| B2 || C || A |
+-+------++-+------++-----++-----+
| | |
V V ^
| | +-Thunk-+ |
+--->>----+-->>---| ->>-+
+-------+
因此,无论您是使用B1&
还是B2&
,您都需要首先查找thunk,然后该thunk会告诉您在哪里可以找到实际的基本子对象。这也解释了为什么不能执行从a&
到任何派生类型的静态强制转换:编译时根本不存在这些信息
要获得更深入的解释,请参阅。(在该描述中,thunk是C
的vtable的一部分,虚拟继承总是需要维护vtables,即使在任何地方都没有虚拟函数。)我对您的代码做了如下调整:
#include <stdio.h>
#include <stdint.h>
struct A {
int a;
A() : a(32) { f(0); }
A(int i) : a(32) { f(i); }
virtual void f(int i) { printf("%d\n", i); }
};
struct B1 : virtual A {
int b1;
B1(int i) : A(i), b1(33) { f(i); }
virtual void f(int i) { printf("%d\n", i+10); }
};
struct B2 : virtual A {
int b2;
B2(int i) : A(i), b2(34) { f(i); }
virtual void f(int i) { printf("%d\n", i+20); }
};
struct C : B1, virtual B2 {
int c;
C() : B1(6),B2(3),A(1), c(35) {}
virtual void f(int i) { printf("%d\n", i+30); }
};
int main() {
C foo;
intptr_t address = (intptr_t)&foo;
printf("offset A = %ld, sizeof A = %ld\n", (intptr_t)(A*)&foo - address, sizeof(A));
printf("offset B1 = %ld, sizeof B1 = %ld\n", (intptr_t)(B1*)&foo - address, sizeof(B1));
printf("offset B2 = %ld, sizeof B2 = %ld\n", (intptr_t)(B2*)&foo - address, sizeof(B2));
printf("offset C = %ld, sizeof C = %ld\n", (intptr_t)(C*)&foo - address, sizeof(C));
unsigned char* data = (unsigned char*)address;
for(int offset = 0; offset < sizeof(C); offset++) {
if(!(offset & 7)) printf("| ");
printf("%02x ", (int)data[offset]);
}
printf("\n");
}
+--------+----+----+--------+----+----+--------+----+----+
| vptr | b1 | c | vptr | a | xx | vptr | b2 | xx |
+--------+----+----+--------+----+----+--------+----+----+
因此,我们可以将布局描述如下:
#include <stdio.h>
#include <stdint.h>
struct A {
int a;
A() : a(32) { f(0); }
A(int i) : a(32) { f(i); }
virtual void f(int i) { printf("%d\n", i); }
};
struct B1 : virtual A {
int b1;
B1(int i) : A(i), b1(33) { f(i); }
virtual void f(int i) { printf("%d\n", i+10); }
};
struct B2 : virtual A {
int b2;
B2(int i) : A(i), b2(34) { f(i); }
virtual void f(int i) { printf("%d\n", i+20); }
};
struct C : B1, virtual B2 {
int c;
C() : B1(6),B2(3),A(1), c(35) {}
virtual void f(int i) { printf("%d\n", i+30); }
};
int main() {
C foo;
intptr_t address = (intptr_t)&foo;
printf("offset A = %ld, sizeof A = %ld\n", (intptr_t)(A*)&foo - address, sizeof(A));
printf("offset B1 = %ld, sizeof B1 = %ld\n", (intptr_t)(B1*)&foo - address, sizeof(B1));
printf("offset B2 = %ld, sizeof B2 = %ld\n", (intptr_t)(B2*)&foo - address, sizeof(B2));
printf("offset C = %ld, sizeof C = %ld\n", (intptr_t)(C*)&foo - address, sizeof(C));
unsigned char* data = (unsigned char*)address;
for(int offset = 0; offset < sizeof(C); offset++) {
if(!(offset & 7)) printf("| ");
printf("%02x ", (int)data[offset]);
}
printf("\n");
}
+--------+----+----+--------+----+----+--------+----+----+
| vptr | b1 | c | vptr | a | xx | vptr | b2 | xx |
+--------+----+----+--------+----+----+--------+----+----+
这里,xx表示填充。注意编译器是如何将变量c
放入其非虚拟基的填充中的。还要注意的是,所有三个v型指针都是不同的,这允许程序推断出所有虚基的正确位置。这是家庭作业还是好奇心?实际上这是考试。但是我确信,如果我最终理解了这个例子中的工作原理,我就能理解与多重继承和虚拟继承相关的任何内容;intptr_t offsetB1=(intptr_t)(B1*)和foo-(intptr_t)和foo代码>,其他基的起始可以类似地导出。另外,计算所有类的sizeof
应该会给你另一个好的线索。谢谢你的回答。据我所知,thunk是虚拟表的一部分。也就是说,如果不需要偏移量来获取对象函数正在处理的对象,则不需要砰的一声。如果需要偏移量,那么在vtable的相应字段中有一个指向thunk的指针,其中包含偏移量和指向实际函数的指针。所以我很想知道,在我的示例中vtables看起来是怎样的。也就是说,它们指向哪一个函数,哪一个函数是通过thunks指向的。同样,我非常惊讶的是,所有的强制转换(静态、动态、重新解释)都将我变成了一个特定的函数C::f。这很奇怪。你能解释一下(在这个例子中)它们是如何工作的吗?另外,我读过很多关于这方面的文章,你链接的那篇文章是我读过的第一篇文章之一。它仍然不能帮助我理解这里发生的事情。@user1544364“所有的强制转换()将我转换为一个特定的函数”否。这些强制转换返回一个对象指针,而不是函数。@user1544364“thunk,它包含偏移量和指向实际函数的指针。”否。thunk不包含数据,thunk由可执行代码组成。thunk只是一个优化功能。指向“精品”的链接已断开,但我找到了备份: