使用虚方法的C ++对象大小

时间:2010-01-10 21:33:02

标签: c++ virtual-method objectsize

我对虚拟对象大小有一些疑问。

1)虚拟功能

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)虚拟继承

    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

尺寸B为16

sizeof C是16

sizeof D是28这是指28 = 16(B类)+ 16(C类) - 8(A类)+ 4(这是什么?)

我的问题是,为什么在应用虚拟继承时会有额外的空间?

在这种情况下,对象大小的下方规则是什么?

虚拟应用于所有基类和部分基类有什么区别?

6 个答案:

答案 0 :(得分:21)

这是所有实现定义的。我正在使用VC10 Beta2。有助于理解这些东西的关键(虚函数的实现),您需要了解Visual Studio编译器中的秘密开关, / d1reportSingleClassLayoutXXX 。我会在一秒钟内完成。

基本规则是对于任何指向对象的指针,vtable需要位于偏移量0处。这意味着多个vtable用于多重继承。

在这里结合问题,我将从顶部开始:

  

是否只有一个vptr,即使B类和A类都有虚函数?为什么只有一个vptr?

这就是虚函数的工作方式,您希望基类和派生类共享相同的vtable指针(指向派生类中的实现。

  

在这种情况下,似乎有两个vptrs在布局中......这是怎么发生的?我认为两个vptrs一个用于A类,另一个用于B类....所以没有vptr用于C类的虚函数?

这是C类的布局,由/ d1reportSingleClassLayoutC报告:

class C size(20):
        +---
        | +--- (base class A)
 0      | | {vfptr}
 4      | | a
        | +---
        | +--- (base class B)
 8      | | {vfptr}
12      | | b
        | +---
16      | c
        +---

你是对的,有两个vtable,每个基类一个。这是它在多重继承中的工作方式;如果C *被转换为B *,指针值将被调整8个字节。 vtable仍然需要偏移0才能使虚函数调用起作用。

A类的上述布局中的vtable被视为C类的vtable(当通过C *调用时)。

  

B的大小是16个字节--------------没有虚拟它应该是4 + 4 + 4 = 12.为什么这里还有4个字节? B级的布局是什么?

这是本例中B类的布局:

class B size(20):
        +---
 0      | {vfptr}
 4      | {vbptr}
 8      | b
        +---
        +--- (virtual base A)
12      | {vfptr}
16      | a
        +---

如您所见,有一个额外的指针来处理虚拟继承。虚拟继承很复杂。

  

D的大小是32个字节--------------它应该是16(B类)+ 12(C类)+ 4(int d)= 32.是吗?

不,36个字节。同样处理虚拟继承。本例中D的布局:

class D size(36):
        +---
        | +--- (base class B)
 0      | | {vfptr}
 4      | | {vbptr}
 8      | | b
        | +---
        | +--- (base class C)
        | | +--- (base class A)
12      | | | {vfptr}
16      | | | a
        | | +---
20      | | c
        | +---
24      | d
        +---
        +--- (virtual base A)
28      | {vfptr}
32      | a
        +---
  

我的问题是,为什么在应用虚拟继承时会有额外的空间?

虚拟基类指针,它很复杂。基类在虚拟继承中“组合”。该类不是将基类嵌入到类中,而是具有指向布局中基类对象的指针。如果您有两个使用虚拟继承的基类(“菱形”类层次结构),它们将指向对象中的相同虚拟基类,而不是具有该基类的单独副本。

  

在这种情况下,对象大小的下面规则是什么?

重点;没有规则:编译器可以做任何需要做的事情。

最后的细节;制作我正在编译的所有这些类布局图:

cl test.cpp /d1reportSingleClassLayoutXXX

其中XXX是您想要查看布局的结构/类的子字符串匹配。使用此方法,您可以自己探索各种继承方案的影响,以及添加填充的原因/位置等。

答案 1 :(得分:3)

考虑它的一个好方法是了解必须采取哪些措施来处理上转。我将尝试通过显示您描述的类的对象的内存布局来回答您的问题。

代码示例#2

内存布局如下:

vptr | A::a | B::b

将指向B的指针上传到类型A将导致相同的地址,并使用相同的vptr。这就是为什么这里不需要额外的vptr。

代码示例#3

vptr | A::a | vptr | B::b | C::c

正如你所看到的,这里有两个vptr,就像你猜到的那样。为什么?因为如果我们从C转发到A,我们不需要修改地址,因此可以使用相同的vptr。但是如果我们从C转发到B,我们需要修改,相应地我们需要在结果对象的开头有一个vptr。

因此,除了第一个之外的任何继承类都需要额外的vptr(除非继承的类没有虚方法,在这种情况下它没有vptr)。

代码示例#4及以上

虚拟派生时,需要一个新的指针,称为基本指针,指向派生类的内存布局中的位置。当然,可以有多个基指针。

那么内存布局如何?这取决于编译器。在您的编译器中,它可能类似于

vptr | base pointer | B::b | vptr | A::a | C::c | vptr | A::a
          \-----------------------------------------^

但是其他编译器可能会在虚拟表中包含基本指针(通过使用偏移量 - 这值得另一个问题)。

你需要一个基本指针,因为当你以虚拟方式派生时,派生类只会在内存布局中出现一次(如果它也正常派生,可能会出现额外的次数,如你的例子),所以它的所有子代必须指向完全相同的位置。

编辑:澄清 - 这一切都取决于编译器,我展示的内存布局在不同的编译器中可能会有所不同。

答案 2 :(得分:3)

报价>我的问题是,关于继承中vptrs数量的规则是什么?

没有规则,允许每个编译器供应商以他认为合适的方式实现继承的语义。

B类:公共A {},大小= 12.这很正常,B的一个vtable有两个虚方法,vtable指针+ 2 * int = 12

C类:公共A,公共B {},大小= 20.C可以任意扩展A或B的vtable。2 * vtable指针+ 3 * int = 20

虚拟继承:这就是你真正触及未记录行为边缘的地方。例如,在MSVC中,#pragma vtordisp和/ vd编译选项变得相关。 this article中有一些背景信息。我研究了几次并决定编译选项的首字母缩写词代表我的代码可能发生的事情,如果我曾经使用它。

答案 3 :(得分:2)

所有这些都是您实现的完全实现定义。你不能指望任何一个。没有“规则”。

在继承示例中,以下是类A和B的虚拟表的外观:

      class A
+-----------------+
| pointer to A::v |
+-----------------+

      class B
+-----------------+
| pointer to A::v |
+-----------------+
| pointer to B::w |
+-----------------+

正如您所看到的,如果您有一个指向B类虚拟表的指针,它也完全有效作为A类的虚拟表。

在你的C类例子中,如果你考虑一下,就没有办法让一个有效的虚拟表作为C类,A类和B类的表。所以编译器会生成两个。一个虚拟表对A类和C类有效(最有可能),另一个对A类和B类有效。

答案 4 :(得分:1)

这显然取决于编译器的实现。 无论如何,我认为我可以从下面链接的经典论文给出的实现中总结以下规则,并给出你在示例中得到的字节数(除了D类,它是36字节而不是32 !!!) :

T类对象的大小为:

  • 其字段的大小加上T为每个对象继承PLUS 4个字节的每个对象的大小之和,如果T需要另外的v-table,则T虚拟继承PLUS 4个字节
  • 注意:如果K类实际上是多次遗传(在任何级别),则必须只添加一次K的大小

所以我们必须回答另一个问题:课程什么时候需要另一个v-table?

  • 只有在具有一个或多个虚拟方法的情况下,不从其他类继承的类才需要v表
  • 另外,只有非虚拟继承的类没有v-table
  • ,类才需要另一个v表。

规则的结束(我认为可以用来匹配Terry Mahaffey在答案中解释的内容):)

无论如何,我的建议是阅读Bjarne Stroustrup(C ++的创建者)的以下论文,它解释了这些事情:虚拟或非虚拟继承需要多少个虚拟表......以及为什么!

这真是一个很好的阅读: http://www.hpc.unimelb.edu.au/nec/g1af05e/chap5.html

答案 5 :(得分:0)

我不确定,但我认为这是因为指向Virtual method table