为什么虚拟关键字会增加派生类的大小

Why does virtual keyword increase the size of derived a class?

本文关键字:派生 增加 虚拟 关键字 为什么      更新时间:2023-10-16

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

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。

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

(我正在处理代码::块 10.05,如果有人真的需要这个)

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

首先,在基类中使用虚函数会增加其指针大小(在本例中为 4 个字节),因为它需要存储指向虚拟方法表的指针:

normal inheritance with virtual functions:
0        4       8       12
|      base      |
| vfptr  |  i    |   j   |

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

virtual inheritance with virtual functions:
0        4         8        12        16
                   |      base        |
|  xxx   |   j     |  vfptr |    i    |

其中xxx是指向某些类型信息记录的指针,它允许确定到base的偏移量。

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

virtual inheritance without virtual functions:
0        4         8        12
                   |  base  |
|  xxx   |   j     |   i    |

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

由于派生类

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

此外,基类

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

如果基类需要 vptr,

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

因此,派生的不能有主基,并且它引入了自己的vptr

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

类型

derived 的完整对象的布局为:{ 类型 derived 的基类子对象,base }

因此,derived的最小可能大小为 (2 int + 2 vptr) 或常见 ptr 上的 4 个单词 = int = 单词架构,或在本例中为 16 个字节。(Visual C++ 会生成更大的对象(当涉及虚拟基类时),我相信derived会多一个指针。

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

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

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

虚拟继承的目的是允许共享基类。问题是这样的:

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 中的基数,还是派生 1 中的基数。

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

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

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

但是想想必须如何实施。通常,在派生 0 中,d0 紧跟在 m 之后,而在派生 1 中,d1 紧跟在 m 之后。但是对于虚拟继承,它们都共享相同的 m,因此您不能让 d0 和 d1 紧随其后。因此,您将需要某种形式的额外间接寻址。这就是额外指针的来源。

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

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

注意:使类派生自析构函数不是虚拟的类通常会带来麻烦。大麻烦。

可能需要额外的 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);
}

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

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