C++对象的数据成员

数据成员的布局

对于一个类来说它的对象中只存放非静态的数据成员,但是除此之外,编译器为了实现virtual功能还会合成一些其它成员插入到对象中。我们来看看这些成员的布局。

C++ 标准的规定

  • 在同一个Access Section(也就是private,public,protected片段)中, 要求较晚出现的数据成员处在较大的内存中。这意味着同一个片段中的数据成员并不需要紧密相连,编译器所做的成员对齐就是一个例子。
  • 允许编译器将多个Acess Section的顺序自由排列,而不必在乎它们的声明 次序。但似乎没有编译器这样做。
  • 对于继承类,C++标准并未指定是其基类成员在前还是自己的成员在前。
  • 对于虚基类成员也是同样的未予规定。

一般的编译器怎么做?

  • 同一个Access Section中的数据成员按期声明顺序,依次排列。 但成员与成员之间因为内存对齐的原因可能存在空当。
  • 多个Access Section按其声明顺序排放。
  • 基类的数据成员总放在自己的数据成员之前,但虚基类除外。

编译器合成的成员放在哪?

为了实现虚函数和虚拟继承两个功能,编译器一般会合成Vptr和Vbptr两个指针。那么这两个指针应该放在什么位置?C++标准肯定是不曾规定的,因为它甚至并没有规定如何来实现这两个功能,因此就语言层面来看是不存在这两个指针的。

对于Vptr来说有的编译器将它放在末尾,如Lippman领导开发的Cfront。有的则将其放在最前面,如MS的VC,但似乎没人将它放在中间。为什么不放在中间?没有理由可以让人这么做,放在末尾,可以保持C++类对C的struct的良好兼容性,放在最前可以给多重继承下的指针或引用调用虚函数带来好处。

看一小段代码:

class X{
public:
    int a;
    virtual void vfc(){};
};
int main()
{
    using namespace std;
    X x;
    cout<<&x.a<<" "<<&x<<endl;
    system("pause");
}

在VS2010和VC6.0中运行的结果都是地址值&x.a比&x大4,可见说vc的vptr放在对象的最前面此言非虚。

对于Vbptr来说,有好几种方法,在这儿我们只看看VC的实现原理:

对于由虚拟继承而得的类,VC会在其每一个对象中插入一个Vbptr,这个Vbptr指向vitual base class table(我称之为虚基类表)。虚基类表中则存放有其虚基类子对象相对于虚基类指针的偏移量。例如声明如class Y:virtual public X的类的virtual base class table的虚基类表中当存储有X对象相对于Vbptr的偏移量。

对象成员或基类对象成员后面的填充空白不能为其它成员所用

看一段代码:

class X{
public:
    int x;
    char c;
};
class X2:public X
{
public:char  c2;
};

X2的布局应当是x(4),c(1),c2(1),这么说来sizeof(X2)的值应该是8?错了,实际上是12。原因在于X后面的三个字节的填充空白不能为c2所用。也就是说X2的大小实际上为:X(8)+c2(1)+填补(3)=12。这样看来编译器似乎是那么的呆板,其实不然,看一下下面的语句会发生什么?

X2 x2;
X x;
x2=x;

如果X后面的填充空白可以被c2使用的话,那么X2和X都将是8字节。上面的语句执行后x2.c2的值会是多少?一个不确定的值!这样的结果肯定不是我们想要的。

Vptr与Vbptr1

  • 在多继承情况下,即使是多虚拟继承,继承而得的类只需维护一个Vbptr; 而多继承情况下Vptr则可能有要维护多个Vptr,视其基类有几个有虚函数。
  • 一条继承线路只有一个Vptr,但可能有多个Vbptr,视有几次虚拟 继承而定。换言之,对于一个继承类对象来说,不需要新合成vptr,而是使用其基类子对象的vptr。而对于一个虚拟继承类来说,必须新合成一个自己的Vbptr。

如:

class X{
    virtual void vf(){};
};
class X2:virtual public X
{
  virtual void vf(){};
};
class X3:virtual public  X2
{
     virtual void vf(){};
}

X3将包含有一个Vptr,两个Vbptr。确切的说这两个Vbptr一个属于X3,一个属于X3的子对象X2,X3通过其Vbptr找到子对象X2,而X2通过其Vbptr找到X。

其中差别在于vptr通过一个虚函数表可以确切地知道要调用的函数,而Vbptr通过虚基类表只能够知道其虚基类子对象的偏移量。这两条规则是由虚函数与虚拟继承的实现方式,以及受它们的存取方式和复制控制的要求决定的。

数据成员的存取

静态数据成员相当于一个仅对该类可见的全局变量,因为程序中只存在一个静态数据成员的实例,所以其地址在编译时就已经被决定。不论如何静态数据成员的存取不会带来任何额外负担。

非静态数据成员的存取,相当于对象起始地址加上偏移量。效率上与C struct成员的效率等同。因为它的偏移量在编译阶段已经确定。但有一种情况例外:pt->x=0.0。当通过指针或引用来存取——x而x又是虚基类的成员的时候。因为必须要等到执行期才能知道pt指向的确切类型,所以必须通过一个间接导引才能完成。

小结

在VC中数据成员的布局顺序为:

  1. vptr部分(如果基类有,则继承基类的)
  2. vbptr (如果需要)
  3. 基类成员(按声明顺序)
  4. 自身数据成员
  5. 虚基类数据成员(按声明顺序)

参考:《深度探索C++对象模型》


  1. 这部分内容只是自己试验而得,并非放诸各编译器皆适合的准则。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值