有这段代码:
#include <iostream>
class Base
{
int x;
};
class Derived : virtual public Base
{
int y;
};
int main()
{
std::cout << sizeof(Derived) << std::endl; // prints 12
return 0;
}
我已经读过,当某个类被虚拟继承时,会为类Derived创建空的vtable,因此内存布局如下:
Derived::ptr to empty vtable
Derived::y
Base::x
它是12个字节。问题是 - 如果没有任何虚拟方法以及如何使用这个空 vtable的目的是什么?
答案 0 :(得分:4)
Derived
需要某种方式来了解Base
子对象的位置。使用虚拟继承时,基类的相对位置不会相对于派生类的位置固定:它可能位于完整对象的任何位置。
考虑一个涉及钻石继承的更典型的例子。
struct A
{
int a;
};
struct B1 : virtual A
{
int b1;
};
struct B2 : virtual A
{
int b2;
};
struct C : B1, B2
{
int c;
};
此处,B1
和B2
实际上都来自A
,因此在C
中,只有一个A
子对象。 B1
和B2
都需要知道如何找到A
子对象(以便他们可以访问a
成员变量或A
的其他成员我们要定义它们)。
在这种情况下,这就是vtable的用途:B1
和B2
都有一个vtable,其中包含A
子对象的偏移量。
为了演示编译器可能会如何实现上述菱形继承示例,请考虑由Visual C ++ 11 Developer Preview生成的以下类布局和虚拟表。
class A size(4):
+---
0 | a
+---
class B1 size(12):
+---
0 | {vbptr}
4 | b1
+---
+--- (virtual base A)
8 | a
+---
class B2 size(12):
+---
0 | {vbptr}
4 | b2
+---
+--- (virtual base A)
8 | a
+---
class C size(24):
+---
| +--- (base class B1)
0 | | {vbptr}
4 | | b1
| +---
| +--- (base class B2)
8 | | {vbptr}
12 | | b2
| +---
16 | c
+---
+--- (virtual base A)
20 | a
+---
以及以下vtable:
B1::$vbtable@:
0 | 0
1 | 8 (B1d(B1+0)A)
B2::$vbtable@:
0 | 0
1 | 8 (B2d(B2+0)A)
C::$vbtable@B1@:
0 | 0
1 | 20 (Cd(B1+0)A)
C::$vbtable@B2@:
0 | 0
1 | 12 (Cd(B2+0)A)
请注意,偏移量与vtable的地址相关,请注意,对于为B1
的{{1}}和B2
子对象生成的两个vtable,偏移量是不同的。
(另请注意,这完全是一个实现细节 - 其他编译器可能会以不同的方式实现虚函数和基础。这个示例演示了它们实现的一种方式,并且它们通常以这种方式实现。)
答案 1 :(得分:1)
为了实现虚函数,C ++使用一种特殊形式的后期绑定,称为虚拟表。虚拟表是用于以动态/后期绑定方式解析函数调用的函数的查找表。虚拟表有时会使用其他名称,例如“vtable”,“虚函数表”,“虚方法表”或“调度表”。
虚拟表实际上非常简单。首先,每个使用虚函数的类(或者从使用虚函数的类派生)都被赋予它自己的虚拟表。该表只是编译器在编译时设置的静态数组。虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的派生函数最多。
即使对于具有虚拟基类的类,也会创建虚拟表。在这种情况下,vtable具有指向基类的共享实例的指针以及指向classe的虚函数的指针(如果有的话)。
答案 2 :(得分:0)
如果你执行dynamic_cast&lt; Derived *&gt;(ptr_to_obj),它将使用vtable指针来确定ptr_to_obj是否引用了Derived。涉及虚拟方法或继承的每个类都需要一个vtable,并且每个类需要不同,以支持dynamic_cast&lt;&gt;。即使它不包含任何指向方法的指针,它仍然用于识别对象的类型。