我对虚拟对象大小有一些疑问 .
1) virtual function
class A {
public:
int a;
virtual void v();
}
A类的大小是8字节....一个整数(4个字节)加上一个虚拟指针(4个字节)很清楚!
class B: public A{
public:
int b;
virtual void w();
}
B级的大小是多少?我使用sizeof B测试,它打印12
这是否意味着只有一个vptr,即使B类和A类都有虚函数?为什么只有一个vptr?
class A {
public:
int a;
virtual void v();
};
class B {
public:
int b;
virtual void w();
};
class C : public A, public B {
public:
int c;
virtual void x();
};
C的大小是20 ........
似乎在这种情况下,两个vptrs在布局中......这是怎么发生的?我认为两个vptrs一个用于A类,另一个用于B类....所以没有vptr用于C类的虚函数?
我的问题是,关于继承中vptrs数量的规则是什么?
2) virtual inheritance
class A {
public:
int a;
virtual void v();
};
class B: virtual public A{ //virtual inheritance
public:
int b;
virtual void w();
};
class C : public A { //non-virtual inheritance
public:
int c;
virtual void x();
};
class D: public B, public C {
public:
int d;
virtual void y();
};
A的大小是8个字节-------------- 4(int a)4(vptr)= 8
B的大小是16个字节--------------没有虚拟它应该是4 4 4 = 12.为什么这里还有4个字节? B级的布局是什么?
C的大小是12个字节 . -------------- 4 4 4 = 12.很明显!
D的大小是32个字节--------------它应该是16(B类)12(C类)4(int d)= 32.是吗?
class A {
public:
int a;
virtual void v();
};
class B: virtual public A{ //virtual inheritance here
public:
int b;
virtual void w();
};
class C : virtual public A { //virtual inheritance here
public:
int c;
virtual void x();
};
class D: public B, public C {
public:
int d;
virtual void y();
};
sizeof A是8
sizeof B是16
sizeof C是16
sizeof D是28这是指28 = 16(B类)16(C类) - 8(A类)4(这是什么?)
我的问题是,为什么在应用虚拟继承时会有额外的空间?
在这种情况下,对象大小的下面规则是什么?
虚拟应用于所有基类和部分基类时有什么区别?
6 回答
这是所有实现定义的 . 我正在使用VC10 Beta2 . 有助于理解这些东西的关键(虚函数的实现),您需要了解Visual Studio编译器中的秘密开关, /d1reportSingleClassLayoutXXX . 我会在一秒钟内完成 .
基本规则是对于任何指向对象的指针,vtable需要位于偏移量0处 . 这意味着多个vtable用于多重继承 .
在这里结合问题,我将从顶部开始:
这就是虚函数的工作方式,您希望基类和派生类共享相同的vtable指针(指向派生类中的实现) .
这是C类的布局,由/ d1reportSingleClassLayoutC报告:
你是对的,有两个vtable,每个基类一个 . 这是它在多重继承中的工作方式;如果C *被转换为B *,指针值将被调整8个字节 . vtable仍然需要在偏移0处才能使虚函数调用起作用 .
上述A类布局中的vtable被视为C类的vtable(当通过C *调用时) .
这是此示例中B类的布局:
如您所见,有一个额外的指针来处理虚拟继承 . 虚拟继承很复杂 .
不,36个字节 . 同样处理虚拟继承 . 本例中D的布局:
虚基类指针,它很复杂 . 基类在虚拟继承中“组合” . 该类不是将基类嵌入到类中,而是具有指向布局中基类对象的指针 . 如果您有两个使用虚拟继承的基类(“菱形”类层次结构),它们将指向对象中的相同虚拟基类,而不是具有该基类的单独副本 .
很重要的一点;没有规则:编译器可以做任何需要做的事情 .
最后的细节;制作我正在编译的所有这些类布局图:
其中XXX是结构/类的子字符串匹配,您希望看到它的布局 . 使用此方法,您可以自己探索各种继承方案的影响,以及添加填充的原因/位置,等等
引用>我的问题是,关于继承中的vptrs数量的规则是什么?
没有规则,允许每个编译器供应商以他认为合适的方式实现继承的语义 .
B级:公共A {},大小= 12.这很正常,B的一个vtable有两个虚方法,vtable指针2 * int = 12
C类:公共A,公共B {},大小= 20.C可以任意扩展A或B的vtable . 2 * vtable指针3 * int = 20
虚拟继承:this article中的's where you really hit the edges of undocumented behavior. For example, in MSVC the #pragma vtordisp and /vd compile options become relevant. There' s一些背景信息 . 我研究了几次,并决定编译选项的首字母缩写词代表我的代码可能发生的事情,如果我曾经使用它 .
思考它的一个好方法是了解必须采取哪些措施来处理上转 . 我将尝试通过显示您描述的类的对象的内存布局来回答您的问题 .
代码示例#2
内存布局如下:
将指向B的指针向上转换为类型A将导致相同的地址,并使用相同的vptr . 这就是为什么这里不需要额外的vptr .
代码示例#3
如您所见,有两个vptr 's here, just like you guessed. Why? Because it'是真的,如果我们从C转发到A,我们不需要修改地址,因此可以使用相同的vptr . 但是如果我们从C转发到B,我们需要修改,相应地我们需要在生成的对象的开头有一个vptr .
因此,除了第一个之外的任何继承类都需要额外的vptr(除非继承的类没有虚方法,在这种情况下它没有vptr) .
代码示例#4及更高版本
虚拟派生时,需要一个名为 base pointer 的新指针来指向派生类的内存布局中的位置 . 当然,可以有多个基指针 .
那么内存布局看起来如何?这取决于编译器 . 在你的编译器中它可能就像
但是其他编译器可能会在虚拟表中包含基本指针(通过使用偏移量 - 这值得另一个问题) .
你需要一个基本指针,因为当你以虚拟方式派生时,派生类只会在内存布局中出现一次(如果它也正常派生,可能会出现额外的次数,如你的例子),所以它的所有子节点都必须指向完全相同的位置 .
编辑:澄清 - 这一切都取决于编译器,我展示的内存布局可能在不同的编译器中有所不同 .
所有这些都是您实现的完全实现定义 . 你不能指望任何一个 . 没有“规则” .
在继承示例中,以下是类A和B的虚拟表的外观:
如您所见,如果您有一个指向B类虚拟表的指针,它也完全有效,因为它是A类的虚拟表 .
在你的C类例子中,如果你考虑一下,就没有办法让一个虚拟表同时作为C类,A类和B类的表有效 . 所以编译器会生成两个 . 一个虚拟表对A类和C类(大部分可能)有效,另一个对A类和B类有效 .
这显然取决于编译器实现 . 无论如何,我认为我可以从下面链接的经典论文给出的实现中总结以下规则,并给出你在示例中得到的字节数(除了D类,它是36字节而不是32 !!!) :
T类对象的大小是:
其字段的大小加上T为每个对象继承PLUS 4个字节的每个对象的大小之和,如果T需要另外的v-table,则T虚拟继承PLUS 4个字节
注意:如果类K实际上是多次遗传(在任何级别),你只需要添加一次K的大小
所以我们必须回答另一个问题:一个类什么时候需要另一个v-table?
只有具有一个或多个虚方法的类不从其他类继承才需要v表
OTHERWISE,一个类只需要另一个v-table,如果它实际上没有继承的类没有v-table
规则的结束(我认为可以应用于匹配Terry Mahaffey在答案中解释的内容):)
无论如何,我的建议是阅读Bjarne Stroustrup(C的创建者)的以下论文,它解释了这些事情:虚拟或非虚拟继承需要多少个虚拟表......以及为什么!
这真是一个很好的阅读:http://www.hpc.unimelb.edu.au/nec/g1af05e/chap5.html
我不确定,但我认为这是因为指针Virtual method table