当作为公共虚拟继承时,为什么类的大小会增加

Why the size of class is increasing when inherit as public virtual?

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

为什么bc类的大小是4 ?virtual关键字是否创建了一个vptr(vptr不带virtual函数存在吗?)还是其他什么?请分享你的想法。

#include<iostream>
using namespace std;

class a{    
};
class b:public virtual a{
};
class c:public virtual a{
};
class d:public  b, public c{
};
main(){
    cout<<sizeof(a)<<"n"; //1
    cout<<sizeof(b)<<"n"; //4
    cout<<sizeof(c)<<"n"; //4
    cout<<sizeof(d)<<"n"; //8
}

如果virtual不在任何地方使用,则o/p变为:1 1 1 2;预期行为。

是的,由于虚拟继承vptr被编译器创建,即使没有虚拟函数。为了理解如何使用gcc编译器,我们可以使用(-fdump-tree-all)标志,并查看中间文件(*.class),其中可以找到vptr和vtable布局。

$ g++ -fdump-tree-all -Wall basic.cpp -o basic

现在我们可以从中间的basic.class文件中找到关于vptr和vtable布局的信息。

//a类信息

Class a
   size=1 align=1
   base size=0 base align=1
a (0x0x7fc8d707e2a0) 0 empty

//b类VPTR和size信息

Vtable for b
b::_ZTV4b: 3u entries
0     0u
8     (int (*)(...))0
16    (int (*)(...))(& _ZTI4bbbb)
VTT for b
b::_ZTT4b: 1u entries
0     ((& b::_ZTV4b) + 24u)
Class b
   size=8 align=8
   base size=8 base align=8
b (0x0x7fc8d7053e38) 0 nearly-empty
    vptridx=0u vptr=((& b::_ZTV4bbbb) + 24u)
  a (0x0x7fc8d707e300) 0 empty virtual
      vbaseoffset=-24

//c类VPTR和size信息

Vtable for c
c::_ZTV4c: 3u entries
0     0u
8     (int (*)(...))0
16    (int (*)(...))(& _ZTI4cccc)
VTT for c
c::_ZTT4c: 1u entries
0     ((& c::_ZTV4c) + 24u)
Class c
   size=8 align=8
   base size=8 base align=8
c (0x0x7fc8d7053ea0) 0 nearly-empty
    vptridx=0u vptr=((& c::_ZTV4c) + 24u)
  a (0x0x7fc8d707e360) 0 empty virtual
      vbaseoffset=-24

//d类VPTR和尺寸信息

Vtable for d
d::_ZTV4d: 6u entries
0     0u
8     (int (*)(...))0
16    (int (*)(...))(& _ZTI4d)
24    18446744073709551608u
32    (int (*)(...))-8
40    (int (*)(...))(& _ZTI4d)
Construction vtable for b (0x0x7fc8d70f8000 instance) in d
d::_ZTC4d0_4b: 3u entries
0     0u
8     (int (*)(...))0
16    (int (*)(...))(& _ZTI4b)
Construction vtable for c (0x0x7fc8d70f8068 instance) in d
d::_ZTC4d8_4c: 3u entries
0     18446744073709551608u
8     (int (*)(...))0
16    (int (*)(...))(& _ZTI4c)
VTT for d
d::_ZTT4d: 4u entries
0     ((& d::_ZTV4d) + 24u)
8     ((& d::_ZTC4d0_4b) + 24u)
16    ((& d::_ZTC4d8_4c) + 24u)
24    ((& d::_ZTV4d) + 48u)
Class d
   size=16 align=8
   base size=16 base align=8
d (0x0x7fc8d70cca80) 0
    vptridx=0u vptr=((& d::_ZTV4d) + 24u)
  b (0x0x7fc8d70f8000) 0 nearly-empty
      primary-for d (0x0x7fc8d70cca80)
      subvttidx=8u
    a (0x0x7fc8d707e3c0) 0 empty virtual
        vbaseoffset=-24
  c (0x0x7fc8d70f8068) 8 nearly-empty
      subvttidx=16u vptridx=24u vptr=((& d::_ZTV4d) + 48u)
    a (0x0x7fc8d707e3c0) alternative-path

这解释了这里发生了什么,以及为什么和如何根据创建的vptr的数量来改变对象的大小。我的机器是x86_64 GNU/Linux,因此指针的大小将是8,而不是原来的例子中的4。

当使用非虚拟继承时,对象的完整布局在编译时确定。当使用虚拟继承时,情况并非如此——在这种情况下,基子对象的偏移量是在运行时确定的。

如何实现的细节会因编译器而异,但通常会涉及一个或多个额外的指针。请看这个问题的答案,有一个解释。

注意,如果你有虚方法,这与虚表指针是分开的。正如您在示例中所指出的,在您的示例中没有虚拟方法。

Stroustrup在c++ 7.1节的多重继承。

表示虚拟基类a对象的对象不能被放置在相对于两者的固定位置所有对象中的bc。因此,指向a的指针必须存储在所有直接访问a的对象中对象,以允许独立于其相对位置的访问。

Scott Meyers的《More Effective c++》,Item 24, "理解虚函数、多重继承、虚基类和RTTI的代价"虽然没有深入讨论这个问题,但解释了虚基类如何通过在两个直接派生类中为虚基类添加指针来增加对象大小。它可能与虚函数没有任何关系。编译器可能会发现这是克服bc本身可能具有不同大小以及a成员访问可以通过b, cd中的任何一个的问题的最简单的方法。

这里有很多"可能",这是因为整个事情完全是特定于编译器的,因为c++标准没有指定类的内存布局。奇怪的是,在互联网上很难找到编译器供应商提供的关于虚拟基类的内存布局的真正好的权威的文档(这可能是因为虚拟继承首先是一个很少使用的c++语言特性)。

对于GCC,您可能会发现-fdump-class-hierarchy编译器选项很有用。以下是它在我的机器上为您的示例生成的内容(删除标准库的内容):

Class a
   size=1 align=1
   base size=0 base align=1
a (0x0x344a0a8) 0 empty
Vtable for b
b::_ZTV1b: 3u entries
0     0u
4     (int (*)(...))0
8     (int (*)(...))(& _ZTI1b)
VTT for b
b::_ZTT1b: 1u entries
0     ((& b::_ZTV1b) + 12u)
Class b
   size=4 align=4
   base size=4 base align=4
b (0x0x3460b40) 0 nearly-empty
    vptridx=0u vptr=((& b::_ZTV1b) + 12u)
  a (0x0x344a0e0) 0 empty virtual
      vbaseoffset=-12
Vtable for c
c::_ZTV1c: 3u entries
0     0u
4     (int (*)(...))0
8     (int (*)(...))(& _ZTI1c)
VTT for c
c::_ZTT1c: 1u entries
0     ((& c::_ZTV1c) + 12u)
Class c
   size=4 align=4
   base size=4 base align=4
c (0x0x3460d40) 0 nearly-empty
    vptridx=0u vptr=((& c::_ZTV1c) + 12u)
  a (0x0x344a118) 0 empty virtual
      vbaseoffset=-12
Vtable for d
d::_ZTV1d: 6u entries
0     0u
4     (int (*)(...))0
8     (int (*)(...))(& _ZTI1d)
12    4294967292u
16    (int (*)(...))-4
20    (int (*)(...))(& _ZTI1d)
Construction vtable for b (0x0x3460f00 instance) in d
d::_ZTC1d0_1b: 3u entries
0     0u
4     (int (*)(...))0
8     (int (*)(...))(& _ZTI1b)
Construction vtable for c (0x0x3460f40 instance) in d
d::_ZTC1d4_1c: 3u entries
0     4294967292u
4     (int (*)(...))0
8     (int (*)(...))(& _ZTI1c)
VTT for d
d::_ZTT1d: 4u entries
0     ((& d::_ZTV1d) + 12u)
4     ((& d::_ZTC1d0_1b) + 12u)
8     ((& d::_ZTC1d4_1c) + 12u)
12    ((& d::_ZTV1d) + 24u)
Class d
   size=8 align=4
   base size=8 base align=4
d (0x0x3460ec0) 0
    vptridx=0u vptr=((& d::_ZTV1d) + 12u)
  b (0x0x3460f00) 0 nearly-empty
      primary-for d (0x0x3460ec0)
      subvttidx=4u
    a (0x0x344a150) 0 empty virtual
        vbaseoffset=-12
  c (0x0x3460f40) 4 nearly-empty
      subvttidx=8u vptridx=12u vptr=((& d::_ZTV1d) + 24u)
    a (0x0x344a150) alternative-path
对于非虚拟继承,内存布局要简单得多:
Class a
   size=1 align=1
   base size=0 base align=1
a (0x0x344a0a8) 0 empty
Class b
   size=1 align=1
   base size=1 base align=1
b (0x0x346cb40) 0 empty
  a (0x0x344a0e0) 0 empty
Class c
   size=1 align=1
   base size=1 base align=1
c (0x0x346cbc0) 0 empty
  a (0x0x344a118) 0 empty
Class d
   size=2 align=1
   base size=2 base align=1
d (0x0x346cc40) 0 empty
  b (0x0x346cc80) 0 empty
    a (0x0x344a150) 0 empty
  c (0x0x346ccc0) 1 empty
    a (0x0x344a188) 1 empty

因此,GCC显然是通过vptrs实现虚拟继承的,并且不关心在不需要时将其优化掉。

我猜B对于vtable更大(即使没有函数)。

实现当然取决于编译器和所有这些,但我记得在Delphi的时候,它会在类的实例前面创建4字节的虚拟表(例如&myInstance-4)。

我猜它必须创建一个表,即使没有函数,也许只是在那里放一个0。

请记住,处理虚函数的方式必须在整个系统中兼容,所以如果你要加载一个动态库来查看你的类,它需要知道如何读取它们。我怀疑,由于这个原因,编译器不能完全摆脱多余的空间。