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; } };
> C实例的内存布局是什么?它包含几个vptrs,其中每个都放置在哪里?哪些虚拟表与C的虚拟表共享?每个虚拟表包含什么?
这里我怎么理解布局:
---------------------------------------------------------------- |vptr1 | AptrOfB1 | b1 | B2ptr | c | vptr2 | AptrOfB2 | b2 | a | ----------------------------------------------------------------
其中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);
为什么所有打电话给f打印33?
解决方法
想象一下,你正在获得一个B& x引用,并且您有责任找到A :: a成员.如果继承是真实的,则B具有超类A,因此通过x查看的B对象具有A子对象,您可以在其中找到您的成员A :: a.如果x的派生最多的对象具有多个类型A的基数,那么您只能看到作为B的子对象的特定副本.
但是,如果继承是虚拟的,这些都不是有意义的.我们不知道我们需要哪个A子对象 – 这个信息在编译时根本不存在.我们可以像B一样处理一个实际的B对象; B& x = y,或与C z对应的C对象; B& x = z;或完全不同的东西,几乎从多个派生出来.唯一的知道方法是在运行时找到实际的基数A.
这可以通过一个更多级别的运行时间间隔来实现. (注意,与非虚函数相比,这与虚拟函数如何实现一个额外的运行时间间隔完全相同).而不是有一个指向vtable或者基本子对象的指针,一个解决方案是存储指向指针的指针到实际的子对象.这有时被称为“笨蛋”或“蹦床”.
所以实际的对象C z;可能看起来如下.内存中的实际排序取决于编译器并且不重要,我已经抑制了vtables.
+-+------++-+------++-----++-----+ |T| B1 ||T| B2 || C || A | +-+------++-+------++-----++-----+ | | | V V ^ | | +-Thunk-+ | +--->>----+-->>---| ->>-+ +-------+
因此,无论你是否有B1&或者B2和amp,你首先查找thunk,那个又反过来告诉你在哪里找到实际的基本子对象.这也解释了为什么你不能从A&到任何派生类型:这个信息在编译时根本不存在.
有关更深入的解释,请查看this fine article.(在该描述中,thunk是C的vtable的一部分,虚拟继承总是需要维护vtables,即使在任何地方都没有虚拟函数).