4

我有两个类 - 一个基类和一个派生自它:

class base {

 int i ;

  public :
  virtual ~ base () { }
};

class derived :  virtual public base { int j ; };

main()

{ cout << sizeof ( derived ) ; }

这里的答案是 16。但是如果我改为使用非虚拟公共继承或使基类非多态,那么我得到的答案是 12,即如果我这样做:

class base {

 int i ;

 public :
virtual ~ base () { }
};

class derived :  public base { int j ; };

main()

{ cout << sizeof ( derived ) ; }

或者

class base {

int i ;

public :
~ base () { }
};

class derived :  virtual public base { int j ; };

main()

{ cout << sizeof ( derived ) ; }

在这两种情况下,答案都是 12。

有人可以解释为什么派生类的大小在第一种和其他两种情况下有所不同吗?

(我在 code::blocks 10.05 工作,如果有人真的需要这个)

4

6 回答 6

3

这里有两件事会导致额外的开销。

首先,在基类中拥有虚函数会增加一个指针大小(在这种情况下为 4 个字节),因为它需要存储指向虚方法表的指针:

normal inheritance with virtual functions:

0        4       8       12
|      base      |
| vfptr  |  i    |   j   |

其次,在虚拟继承中需要额外的信息derived才能定位base。在正常继承中,derived和之间的偏移量base是编译时间常数(0 表示单继承)。在虚拟继承中,偏移量可以取决于对象的运行时类型和实际类型层次结构。实现可能会有所不同,但例如 Visual C++ 会这样做:

virtual inheritance with virtual functions:

0        4         8        12        16
                   |      base        |
|  xxx   |   j     |  vfptr |    i    |

Wherexxx是指向某些类型信息记录的指针,它允许确定到base.

当然,也可以在没有虚函数的情况下进行虚继承:

virtual inheritance without virtual functions:

0        4         8        12
                   |  base  |
|  xxx   |   j     |   i    |
于 2012-06-05T21:17:56.733 回答
3

如果一个类有任何虚函数,那么这个类的对象需要有一个vptr,它是一个指向vtable的指针,也就是可以从中找到正确虚函数地址的虚表。调用的函数取决于对象的动态类型,它是该对象作为基子对象的最派生类。

因为派生类实际上继承自基类,所以基类相对于派生类的位置不是固定的,它也取决于对象的动态类型。使用 gcc,具有虚拟基类的类需要一个 vptr 来定位基类(即使没有虚拟函数)。

此外,基类包含一个数据成员,它位于基类 vptr 之后。基类内存布局为:{ vptr, int}

如果基类需要 vptr,则从它派生的类也需要 vptr,但通常会重用基类子对象的“第一个”vptr(这个具有重用 vptr 的基类称为主基类)。但是在这种情况下这是不可能的,因为派生类需要一个 vptr 不仅要确定如何调用虚函数,还要确定虚基在哪里。派生类在不使用 vptr 的情况下无法定位其虚拟基类;如果将虚拟基类用作主基类,则派生类将需要定位其主基类以读取 vptr,并且需要读取 vptr 以定位其主基类

所以派生不能有一个主要的基础,它引入了自己的 vptr

因此,类型的基类子对象的布局derived是: { vptr, int} 其中 vptr 指向一个用于派生的 vtable,不仅包含虚函数的地址,还包含其所有虚基类的相对位置(这里只是base) ,表示为偏移量。

一个完整的类型对象的布局derived是:{ 类型的基类子对象derivedbase}

所以最小可能的大小derived是 (2 int+ 2 vptr) 或常见 ptr = int= 字架构上的 4 个字,或在这种情况下为 16 个字节。(而且 Visual C++ 会生成更大的对象(当涉及到虚拟基类时),我相信 aderived会有更多的指针。)

所以是的,虚函数是有代价的,而虚拟继承是有代价的。在这种情况下,虚拟继承的内存成本是每个对象多一个指针。

在具有许多虚拟基类的设计中,每个对象的内存成本可能与虚拟基类的数量成正比,或者不成正比;我们需要讨论特定的类层次结构来估算成本。

在没有多重继承或虚拟基类(甚至虚拟函数)的设计中,您可能必须模拟编译器为您自动完成的许多事情,使用一堆指针,可能是指向函数的指针,可能是偏移量......这可能会得到令人困惑和容易出错。

于 2012-08-05T07:36:06.503 回答
2

发生的事情是用于将类标记为具有虚拟成员或涉及虚拟继承的额外开销。多少额外取决于编译器。

一个警告:让一个类从析构函数不是虚拟的类派生通常是自找麻烦。大麻烦。

于 2012-06-05T19:34:45.217 回答
2

在运行时可能需要额外的 4 个字节来标记类类型。例如:

class A {
 virtual int f() { return 2; }
}

class B : virtual public A {
 virtual int f() { return 3; }
}

int call_function( A *a) {
   // here we don't know what a really is (A or B)
   // because of this to call correct method
   // we need some runtime knowledge of type and storage space to put it in (extra 4 bytes).
   return a->f();
}

int main() {
   B b;
   A *a = (A*)&b;

   cout << call_function(a);
}
于 2012-06-05T19:47:29.517 回答
2

虚拟继承的重点是允许共享基类。这是问题所在:

struct base { int member; virtual void method() {} };
struct derived0 : base { int d0; };
struct derived1 : base { int d1; };
struct join : derived0, derived1 {};
join j;
j.method();
j.member;
(base *)j;
dynamic_cast<base *>(j);

最后4行都是模棱两可的。您必须明确是要在 derived0 中使用 base,还是在 derived1 中使用 base。

如果您按如下方式更改第二行和第三行,问题就会消失:

struct derived0 : virtual base { int d0; };
struct derived1 : virtual base { int d1; };

您的 j 对象现在只有一个 base 副本,而不是两个副本,因此最后 4 行不再模棱两可。

但请考虑如何实施。通常,在derived0 中,d0 在m 之后,而在derived1 中,d1 在m 之后。但是对于虚拟继承,它们都共享相同的 m,所以你不能让 d0 和 d1 都紧随其后。所以你需要某种形式的额外间接。这就是额外指针的来源。

如果您想确切地知道布局是什么,这取决于您的目标平台和编译器。仅仅“gcc”是不够的。但是对于许多现代非 Windows 目标,答案是由 Itanium C++ ABI 定义的,它记录在http://mentorembedded.github.com/cxx-abi/abi.html#vtable中。

于 2012-06-05T21:29:43.343 回答
0

额外的大小是由于 vtable/vtable 指针“不可见地”添加到您的类中,以便为此类的特定对象或其后代/祖先保存成员函数指针。

如果不清楚,您需要阅读更多有关 C++ 中虚拟继承的内容。

于 2012-06-05T20:17:17.910 回答