深度探索c++对象模型(4)

从最简单的问题开始

class X{};
class Y:virtual public X{};
class Z:virtual public X{};
class A:public Y, public Z{};

每个类的大小是多少呢?我在vs2015上面的得到的答案是这样的(这里我使用了一个小技巧,在vs中,在项目——属性——配置属性——C/C++——命令行——其他选项中添加选项“/d1reportAllClassLayout”。再次编译时候,编译器会输出所有定义类的对象模型。)

1>  class X size(1):
1>  class Y size(4):
1>   0  | {vbptr}
1>      +---
1>      +--- (virtual base X)
1>  class Z size(4):
1>      +---
1>   0  | {vbptr}
1>      +---
1>      +--- (virtual base X)
1>  class A size(8):
1>      +---
1>   0  | +--- (base class Y)
1>   0  | | {vbptr}
1>      | +---
1>   4  | +--- (base class Z)
1>   4  | | {vbptr}
1>      | +---
1>      +---
1>      +--- (virtual base X)

我们可以看到这里的X的大小是1,这是被编译器安插进去的一个char。这使得class的两个不同的对象可以在内存中得到不同的地址。

我们再来看Y和Z。首先我们要明白的是实现虚继承,将要带来一些额外的负担——额外需要一个某种形式的指针。到目前为止,对于一个32位的机器来说Y、Z的大小应该为5,而不是8或者4。我们需要再考虑两点因素:内存对齐(alignment)和编译器的优化。

考虑内存对齐的原因,Y和Z的大小会增加到4的倍数,也就是相应的补齐3bytes。但是在现在大多数的编译器上做了这样的一个优化处理。因为既然我的类里面有了成员,那么就不需要为空的类按插一个char,那么这是类的大小就是4,所以也就不用3bytes去填补,所以最终Y和Z的大小就是4。关于内存对齐的问题,我不准备在本文展开。大家有兴趣可以参看我的另一篇文章关于内存对齐的总结

关于单继承问题

我们先考虑一个简单的问题,单一继承并且没有virtual function

class X 
{
    int x;
};
class Y :public X 
{
    int y;
};

得到的结果是

1>  class X size(4):
1>      +---
1>   0  | x
1>      +---
1>
1>  class Y size(8):
1>      +---
1>   0  | +--- (base class X)
1>   0  | | x
1>      | +---
1>   4  | y
1>      +---

不能看出子类中继承了父类的成员,现在我们在此基础上增加一个虚函数看看

class X 
{
    int x;
public:
    virtual void funX() {}
};
class Y :public X 
{
    int y;
public:
    virtual void funY(){}
};

结果为

1>  class X size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | x
1>      +---
1>
1>  X::$vftable@:
1>      | &X_meta
1>      |  0
1>   0  | &X::funX
1>
1>  X::funX this adjustor: 0
1>
1>  class Y size(12):
1>      +---
1>   0  | +--- (base class X)
1>   0  | | {vfptr}
1>   4  | | x
1>      | +---
1>   8  | y
1>      +---
1>
1>  Y::$vftable@:
1>      | &Y_meta
1>      |  0
1>   0  | &X::funX
1>   1  | &Y::funY
1>
1>  Y::funY this adjustor: 0

注意这个结果里面多出来了一个东西vftable很明显这个根据字面意思可以知道,这是虚函数指针对应的虚函数表。这里还有另一个东西adjustor,这是什么?

adjustor表示虚函数机制执行时,this指针的调整量,假如fun被多态调用的话,那么它的形式如下:

*(this+0)[0]()

总结虚函数调用形式,应该是:

*(this指针+调整量)[虚函数在vftable内的偏移]()

还有最后这个结果中要说的最后一点是,关于vfptr的位置问题,如你所见,在vs2015中它是被编译器放在了类的最前面,但是不是所有的编译器都是这样的。ok,开始下一个话题。

多重继承问题

依然从最简单的开始,不考虑virtual function

class X 
{
    int x;
};
class Y :public X 
{
    int y;
};
class Z :public X
{
    int z;
};
class A :public Y, public Z
{
    int a;
};

结果是

1>  class X size(4):
1>      +---
1>   0  | x
1>      +---
1>
1>  class Y size(8):
1>      +---
1>   0  | +--- (base class X)
1>   0  | | x
1>      | +---
1>   4  | y
1>      +---
1>
1>  class Z size(8):
1>      +---
1>   0  | +--- (base class X)
1>   0  | | x
1>      | +---
1>   4  | z
1>      +---
1>
1>  class A size(20):
1>      +---
1>   0  | +--- (base class Y)
1>   0  | | +--- (base class X)
1>   0  | | | x
1>      | | +---
1>   4  | | y
1>      | +---
1>   8  | +--- (base class Z)
1>   8  | | +--- (base class X)
1>   8  | | | x
1>      | | +---
1>  12  | | z
1>      | +---
1>  16  | a
1>      +---

我们这里注意到A的大小是20,结合结构图不难理解,好,现在增加virtual function

class X 
{
    int x;
public:
    virtual void funX() {}
};
class Y :public X 
{
    int y;
public:
    virtual void funY(){}
};
class Z :public X
{
    int z;
public:
    virtual void funZ() {}
};
class A :public Y, public Z
{
    int a;
public:
    virtual void funA() {}
    virtual void funY() {}
    virtual void funZ() {}
};

结果是

1>  class X size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | x
1>      +---
1>
1>  X::$vftable@:
1>      | &X_meta
1>      |  0
1>   0  | &X::funX
1>
1>  X::funX this adjustor: 0
1>
1>  class Y size(12):
1>      +---
1>   0  | +--- (base class X)
1>   0  | | {vfptr}
1>   4  | | x
1>      | +---
1>   8  | y
1>      +---
1>
1>  Y::$vftable@:
1>      | &Y_meta
1>      |  0
1>   0  | &X::funX
1>   1  | &Y::funY
1>
1>  Y::funY this adjustor: 0
1>
1>  class Z size(12):
1>      +---
1>   0  | +--- (base class X)
1>   0  | | {vfptr}
1>   4  | | x
1>      | +---
1>   8  | z
1>      +---
1>
1>  Z::$vftable@:
1>      | &Z_meta
1>      |  0
1>   0  | &X::funX
1>   1  | &Z::funZ
1>
1>  Z::funZ this adjustor: 0
1>
1>  class A size(28):
1>      +---
1>   0  | +--- (base class Y)
1>   0  | | +--- (base class X)
1>   0  | | | {vfptr}
1>   4  | | | x
1>      | | +---
1>   8  | | y
1>      | +---
1>  12  | +--- (base class Z)
1>  12  | | +--- (base class X)
1>  12  | | | {vfptr}
1>  16  | | | x
1>      | | +---
1>  20  | | z
1>      | +---
1>  24  | a
1>      +---
1>
1>  A::$vftable@Y@:
1>      | &A_meta
1>      |  0
1>   0  | &X::funX
1>   1  | &A::funY
1>   2  | &A::funA
1>
1>  A::$vftable@Z@:
1>      | -12
1>   0  | &X::funX
1>   1  | &A::funZ
1>
1>  A::funA this adjustor: 0
1>  A::funY this adjustor: 0
1>  A::funZ this adjustor: 12

这里出现一个有意思的问题,在多重继承下,子类有两个虚函数表,分别来自两个父类,其中一个虚函数表中包含了子类的虚函数,而另一个没有。如果子类重写了任意父类的虚函数,都会覆盖对应的函数地址记录。

在这里我们发现A::funZ的函数对应的adjustor值是12,按照我们前边的规则,可以发现该函数的多态调用形式为:

*(this+12)[1]()

此处的调整量12正好是Z的vfptr在A对象内的偏移量。

这里我们注意到在子类A中有两个X,分别是Y中的X和Z中的X,这种做法我们认为是对内存的一种浪费,怎么解决这个问题呢?就引出了虚拟继承。

虚拟继承

class X 
{
    int x;
public:
    virtual void funX() {}
};
class Y :virtual public X 
{
    int y;
public:
    virtual void funY(){}
};
class Z :virtual public X
{
    int z;
public:
    virtual void funZ() {}
};
class A :public Y, public Z
{
    int a;
public:
    virtual void funA() {}
    virtual void funY() {}
    virtual void funZ() {}
};

结果是

1>  class X size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | x
1>      +---
1>
1>  X::$vftable@:
1>      | &X_meta
1>      |  0
1>   0  | &X::funX
1>
1>  X::funX this adjustor: 0
1>
1>  class Y size(20):
1>      +---
1>   0  | {vfptr}
1>   4  | {vbptr}
1>   8  | y
1>      +---
1>      +--- (virtual base X)
1>  12  | {vfptr}
1>  16  | x
1>      +---
1>
1>  Y::$vftable@Y@:
1>      | &Y_meta
1>      |  0
1>   0  | &Y::funY
1>
1>  Y::$vbtable@:
1>   0  | -4
1>   1  | 8 (Yd(Y+4)X)
1>
1>  Y::$vftable@X@:
1>      | -12
1>   0  | &X::funX
1>
1>  Y::funY this adjustor: 0
1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp
1>                 X      12       4       4 0
1>
1>  class Z size(20):
1>      +---
1>   0  | {vfptr}
1>   4  | {vbptr}
1>   8  | z
1>      +---
1>      +--- (virtual base X)
1>  12  | {vfptr}
1>  16  | x
1>      +---
1>
1>  Z::$vftable@Z@:
1>      | &Z_meta
1>      |  0
1>   0  | &Z::funZ
1>
1>  Z::$vbtable@:
1>   0  | -4
1>   1  | 8 (Zd(Z+4)X)
1>
1>  Z::$vftable@X@:
1>      | -12
1>   0  | &X::funX
1>
1>  Z::funZ this adjustor: 0
1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp
1>                 X      12       4       4 0
1>
1>  class A size(36):
1>      +---
1>   0  | +--- (base class Y)
1>   0  | | {vfptr}
1>   4  | | {vbptr}
1>   8  | | y
1>      | +---
1>  12  | +--- (base class Z)
1>  12  | | {vfptr}
1>  16  | | {vbptr}
1>  20  | | z
1>      | +---
1>  24  | a
1>      +---
1>      +--- (virtual base X)
1>  28  | {vfptr}
1>  32  | x
1>      +---
1>
1>  A::$vftable@Y@:
1>      | &A_meta
1>      |  0
1>   0  | &A::funY
1>   1  | &A::funA
1>
1>  A::$vftable@Z@:
1>      | -12
1>   0  | &A::funZ
1>
1>  A::$vbtable@Y@:
1>   0  | -4
1>   1  | 24 (Ad(Y+4)X)
1>
1>  A::$vbtable@Z@:
1>   0  | -4
1>   1  | 12 (Ad(Z+4)X)
1>
1>  A::$vftable@X@:
1>      | -28
1>   0  | &X::funX
1>
1>  A::funA this adjustor: 0
1>  A::funY this adjustor: 0
1>  A::funZ this adjustor: 12
1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp
1>                 X      28       4       4 0

结果比较复杂,用一张图概括就是



我们可以看到。这里的class A内部只有一个class X了,这张图有个小的错误,就是y和z的vbtable后面少画了一个0。这个0是作为一种标记,标记这个表的结束位置。vbtable表中的-4指的是y和z的vbtable相对于自身的偏移。24表示的是y的vbtable距离x的vfptr的偏移,同样的12表示的是z的vbtable距离x的vfptr的偏移。至此所有问题全部解决了?

等一下,还有一个问题呢!!!编译器生成的表中有的虚函数表的前面都会有这样 _meta 结尾,这个是什么呢?深度探索c++对象模型(7)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值