内存对齐与sizeof

一、一个例子
#include <iostream>
 using namespace std;
 struct X1
 {
   int i;//4个字节
   char c1;//1个字节
   char c2;//1个字节
 };
 struct X2
 {
   char c1;//1个字节
   int i;//4个字节
   char c2;//1个字节
 };
 struct X3
 {
   har c1;//1个字节
   char c2;//1个字节
   int i;//4个字节
 };
 int main()
 {   
     X1 x1;
     X2 x2;
     X3 x3;
     cout<<"x1 的大小 "<<sizeof(x1)<<"\n";
     cout<<"x2 的大小 "<<sizeof(x2)<<"\n";
     cout<<"x3 的大小 "<<sizeof(x3)<<"\n";
     return 0;
 }

程序结果:

x1 的大小 8
x2 的大小 12
x3 的大小 8

分析图:

x1:

mem2.jpg

x2:

mem4.jpg

x3:

mem3.jpg


二、struct结构

struct T
{
  char ch;
  int i;
};

    使用sizeof(T),将得到什么样的答案呢?实践出真知,答案8个字节。

    许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐,k则被称为该数据类型的对齐模数(alignment modulus)。当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T严格,而称T比S宽松。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。

    比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。

    ANSI C标准中并没有规定,相邻声明的变量在内存中一定要相邻。为了程序的高效性,内存对齐问题由编译器自行灵活处理,这样导致相邻的变量之间可能会有一些填充字节。对于基本数据类型(int char),他们占用的内存空间在一个确定硬件系统下有个确定的值,所以,接下来我们只是考虑结构体成员内存分配情况。


1.普通策略

  Win32平台下的微软C编译器(cl.exe for 80×86)的对齐策略:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节(trailing padding)。
备注:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。

 

    而在GNU GCC编译器中,遵循的准则有些区别,对齐模数不是像上面所述的那样,根据最宽的基本数据类型来定。在GCC中,对齐模数的准则是:对齐模数最大只能是4,也就是说,即使结构体中有double类型,对齐模数还是4,所以对齐模数只能是1,2,4。而且在上述的三条中,第2条里,offset必须是成员大小的整数倍,如果这个成员大小小于等于4则按照上述准则进行,但是如果大于4了,则结构体每个成员相对于结构体首地址的偏移量(offset)只能按照是4的整数倍来进行判断是否添加填充。
看如下例子:

struct T
{
   char ch;
   double d;
};

那么在GCC下,sizeof(T)应该等于12个字节。

2.位域

关于位域,可以参考另一篇随笔 http://www.cnblogs.com/RoyCNNK/p/3319437.html

如果结构体中含有位域(bit-field),那么VC中准则又要有所更改:
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式(不同位域字段存放在不同的位域类型字节中),Dev-C++和GCC都采取压缩方式;
这部分也可以参考位域那篇随笔的内容。

备注:当两字段类型不一样的时候,对于不压缩方式,例如:

struct N
{
  char c:2;
   int i:4;
};

依然要满足不含位域结构体内存对齐准则第2条,i成员相对于结构体首地址的偏移应该是4的整数倍,所以c成员后要填充3个字节,然后再开辟4个字节的空间作为int型,其中4位用来存放i,所以上面结构体在VC中所占空间为8个字节;而对于采用压缩方式的编译器来说,遵循不含位域结构体内存对齐准则第2条,不同的是,如果填充的3个字节能容纳后面成员的位,则压缩到填充字节中,不能容纳,则要单独开辟空间,所以上面结构体N在GCC或者Dev-C++中所占空间应该是4个字节。

4) 如果位域字段之间穿插着非位域字段,则不进行压缩;
备注:
结构体

typedef  struct
{
    char c:2;
    double i;
    int c2:4;
}N3;

在GCC下占据的空间为16字节,在VC下占据的空间应该是24个字节。

5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。

3. 结构与结构的嵌套:

  对齐模数的选择只能是根据基本数据类型,所以对于结构体中嵌套结构体,只能考虑其拆分的基本数据类型。而对于对齐准则中的第2条,却是要将整个结构体看成是一个成员,成员大小按照该结构体根据对齐准则判断所得的大小。
  照样举个例子:

struct A
{
  char a;
  int b;
  struct B
  {
    char aa:3;
    int bb:6;
    short cc;
    double dd;
  }sb;
  double c; //注释与不注释的结果和预料中不相同
  short d:2;
}sa;

不注释double c这句的结果sizeof(sa)=48,因为对齐模数是8;
而注释掉double c这句之后的结果是sizeof(sa)=40,因为在sb中有double dd,因此对齐模数依然是8。
在struct B中的size计算是独立的。

三、类
在没有虚拟函数的情况下。对象的内存布局比较简单。看下面的类:

class A

{

public:

  int m_a;

  int m_b;

private:

  int m_c;

public:

  int GetValue(){ return m_c; }

};

    实例化一个A的对象a,通过sizeof(a)我们知道这个对象的大小是12字节(3*4)。那么在这个12字节大小的内存空间,对象是怎样布局它的成员变量的呢?很简单,就是按成员变量的定义顺序来安排成员变量在对象内存中的位置。使用(int*)&a可以得到对象a的地址,这个地址就是对象内存空间的首地址,这个地址也就是成员变量m_a的地址。怎样得到m_b的地址呢?(int*)&a+1就行了。同理(int*)&a+2就是m_c的地址了。看到这里你会突然想到什么呢?没错,我们居然可以访问到对象的私有成员。使用*((int*)&a+2) = 4可以为m_c赋值。虽然这很不符合规矩且破坏了面向对象中的封装,不过这可以帮助我们理解对象的内存布局,从这里也可以感受到C++强大的灵活性。

    可能你也很想知道成员函数GetValue的地址是放在哪里的。成员函数的地址并不是在对象的内存空间的,它和其它类外的函数一样,编译器会安排它们到某个内存空间。同样,使用一些“手段”你也可以获取成员函数的地址。由于我对这个也不是很了解,特别是怎么获取私有非虚成员函数的地址,所以这里就跳过不说了。

    下面再说说内存对齐,看下面的类:

class B

{

public:

  int m_a;

  short int m_b;

  double m_c

private:

  int m_d;

public:

  int GetValue(){ return m_c; }

};

    大家可以猜一下类B对象的大小。如果你猜是18(4+2+8+4),也不能说你错。但是你用sizeof(B)一看,结果竟然是24!多出来的6个字节是怎么回事呢?其实是内存对齐搞的鬼。在编译器的结构成员对齐设置为默认的情况下,分配给各个成员变量的内存大小似乎是向占最大空间的成员变量对齐的(这里我不敢肯定,还没看到权威的说法)。在B类中,首先为m_a分配空间,编译器一次为m_a分配8个字节(与最大成员m_c对齐),实际上m_a只占4个字节,还有4个字节多。接着编译器为m_b分配空间,经检查,m_b只占2字节,刚好前面还有4个字节多,所以m_b就放在前面多出的那4个字节空间中,现在已经为m_a和m_b分配了空间,但是m_a加上m_b也就只有6字节,还有2个字节多。如果下面分配的变量刚好是2字节的话,那就刚刚好装满8个字节,没有浪费空间,可是下面是要为double类型的变量分配空间,m_c占了8个字节,显然2个字节是装不下的,因此编译器再为m_c分配了8个字节的空间,刚装满。接下来又为m_d分配空间,根据之前的规则,编译器分配给m_d的空间也是8字节。这样看来,编译器总共为B的对象分配了8+8+8=24字节的空间。可能你觉得编译器这样做是浪费内存空间,但实际上这样做是很适合CPU做一些指令操作的,具体是怎样我不知道,一句话:用空间效率来换时间效率。如果你还是觉得空间比较重要,那么你可以通过设置编译属性或使用编译器指令#pragma来指定编译器所做的对齐方式,例如语句:#pragma pack(1)就是设置向1字节对齐。这时使用sizeof(B)得出的结果就是18了。

 

    现在来看有虚拟函数的情况,看下面的类:

class C

{

public:

  int m_a;

  int m_b;

private:

  int m_c;

private:

  virtual int GetValue(){ cout<<”I got it”<<endl; return 0; }

};

    在编译器的结构成员对齐设置为默认的情况下,sizeof(C)的值是16;因为编译器为我们多产生了一个指向虚函数表(VTABLE)的指针(__vfptr)。这个很容易理解。现在有一个任务,就是想办法调用类C的私有函数GetValue()。为了调用这个私有函数,我们要想办法得到它的入口地址。于是我们会想到先定义一个函数指针,它将会用来保存这个入口地址,例如:

  typedef int (*FUNC)();

  FUNC pFunc;//产生一个函数指针。

    接下来就是把GetValue的入口地址给找出来。我们想到虚函数是放在VTABLE中,那么我们就要想办法得到指向VTABLE的指针__vfptr。还好,__vfptr就在对象内存中首地址,它比m_a还要前,先实例化C的一个对象objc,我们使用(int*)&objc就可以得到一个指向__vfptr的指针了。然后(int*)*(int*)&objc就得到了__vfptr,接着我们就可以使用__vfptr来访问VTABLE中所保存的函数入口地址了。指针再深入(int*)*(int*)*(int*)&objc,这时候我们就得到了VTABLE中第一格(其实还有第0格,用来保存类型信息的)的函数地址,也就是GetValue()的入口地址。终于入了虎穴也得到了虎子,是该走人的时候啦,嗯,还得找个东西来装虎子吧,哈哈,笼子就是前面定义的那个函数指针pFunc。pFunc=(FUNC) (int*)*(int*)*(int*)&objc。OK大功告成!接着调用一下pFunc(),看看会输出什么?I got it!。

    最后,讲讲在有__vfptr的情况下的内存布局。在编译器的结构成员对齐设置为默认的情况下,__vfptr似乎很霸道,它所占有的空间不能和下一个成员变量共享。前面说过,当为一个成员变量分配空间的时候,编译器会检查分配给之前那个成员变量的空间是否还有剩,如果有且可以容纳的下当前的成员变量,那么编译器就会把当前成员变量装进之前多出来的空间中。但是编译器对__vfptr所处在的空间处理就不一样。如果最大成员变量的大小为8,那么编译器首先为__vfptr分配8个字节空间,还有4个多出来。但是这多出来的4个字节不会和下一个成员变量分享,无论下一个成员变量是多大,编译器都是重新为它分配空间。嗯,__vfptr在默认的情况下真好享受,一个人霸占了整个“房间”。不过一使用#pragma pack(1),__vfptr就没有这种特权了。

=======================================================

发几个实测的例子:

class A
{
  virtual void f1();
  int b;
};

class B
{
  virtual void f1();
  virtual void f2();
  int b;
  double a;
};

sizeof(A)=8
sizeof(B)=24, 加上#pragma pack(1)为16

struct S
{
  char a;
  short b;
  int c;
}s;

若s.a地址为0,则s.b地址为2,s.c地址为4。加上#pragma pack(1)后地址连续。

========================================================

这一篇是关于含有继承结构的类的sizeof的补充,一定要看!!!一定要看!!!会令人大吃一惊的!!! http://www.cnblogs.com/caixu/archive/2011/10/11/2207423.html

转载于:https://www.cnblogs.com/RoyCNNK/articles/3351410.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值