29

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

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

这是否意味着即使B类和A类都具有虚功能,也只有一个vptr?为什么只有一个 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......

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

我的问题是,关于继承中 vptr 数量的规则是什么?

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();
    };

A 的大小为 8

B 的大小为 16

C 的大小为 16

sizeof D 为 28 是否意味着 28 = 16(B 类) + 16(C 类) - 8(A 类) + 4 (这是什么?)

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

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

将 virtual 应用于所有基类和部分基类有什么区别?

4

6 回答 6

23

这是所有实现定义的。我正在使用 VC10 Beta2。帮助理解这些东西的关键(虚函数的实现),你需要知道 Visual Studio 编译器中的一个秘密开关,/d1reportSingleClassLayoutXXX。我马上就会讲到。

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

这里有几个问题,我将从顶部开始:

这是否意味着即使B类和A类都具有虚功能,也只有一个vptr?为什么只有一个 vptr?

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

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

这是 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 是您想要查看其布局的结构/类的子字符串匹配项。使用它,您可以自己探索各种继承方案的影响,以及添加填充的原因/位置等。

于 2010-01-10T21:54:25.957 回答
3

考虑它的一个好方法是了解处理向上转换必须做的事情。我将尝试通过显示您描述的类的对象的内存布局来回答您的问题。

代码示例 #2

内存布局如下:

vptr | 一个::一个 | 乙::乙

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

代码示例 #3

vptr | 一个::一个 | vptr | 乙::b | C::c

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

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

代码示例 #4 及以后

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

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

vptr | 基指针 | 乙::b | vptr | 一个::一个 | C::c | vptr | 一个::一个
          \------------------------------------------^

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

您需要一个基指针,因为当您以虚拟方式派生时,派生类将仅在内存布局中出现一次(如果它也正常派生,它可能会出现更多次,如您的示例所示),因此它的所有子类都必须指向完全相同的位置。

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

于 2010-01-10T22:00:41.527 回答
3

Quote> 我的问题是,继承中的vptr数量的规则是什么?

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

B类:public A {},size = 12。这很正常,B的一个vtable具有两个虚拟方法,vtable指针+ 2 * int = 12

class C : public A, public B {}, size = 20. C 可以任意扩展 A 或 B 的 vtable。 2*vtable 指针 + 3*int = 20

虚拟继承:这就是你真正触及无证行为边缘的地方。例如,在 MSVC 中,#pragma vtordisp 和 /vd 编译选项变得相关。这篇文章中有一些背景信息。我对此进行了几次研究,并决定编译选项首字母缩写词代表了如果我曾经使用过我的代码可能会发生什么。

于 2010-01-10T22:12:28.510 回答
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 类有效。

于 2010-01-10T21:52:17.267 回答
1

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

T 类对象的大小为:

  • 其字段的大小加上 T 继承的每个对象的大小的总和,加上 T 虚拟继承的每个对象的 4 个字节加上 4 个字节(仅当 T 需要另一个 v 表时)
  • 注意:如果一个类 K 被多次继承(在任何级别),您只需添加一次 K 的大小

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

  • 一个不从其他类继承的类只有在它有一个或多个虚拟方法时才需要一个 v-table
  • 否则,一个类需要另一个 v-table 只有当它非虚拟继承的类都没有 v-table 时

规则的终结(我认为可以应用它来匹配 Terry Mahaffey 在他的回答中解释的内容):)

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

这真是一本好书:http: //www.hpc.unimelb.edu.au/nec/g1af05e/chap5.html

于 2010-01-10T22:32:17.687 回答
0

我不确定,但我认为这是因为指向虚拟方法表的指针

于 2010-01-10T21:47:08.700 回答