C语言字节对齐详解



C语言字节内存对齐

程序在运行时会将数据临时存放在内存中,芯片内核需要对这些数据进行计算,不断的读取内存以获得数据,并将计算结果写入内存。计算机体系经过若干年的发展,最终确定了以8bits作为其基本的存储单元——byte(字节),这是每个地址所对应的最小访问单元,在C语言中对应一个char型的变量,但是我们常用的字节有8字节、16字节、32字节等,所以在某些情况下字节对其就显得很重要了。

最近在做基于Freakz profile ID的匹配测试,系统用的是Contiki,由于ZigBeeSPEC里规定的MatchDescription Request的包结构体如下:


从上表中可以看出,从上到下依次占用的字节数为16bit16bit8bit2bit*N8bit2bit*N;而Freakz里定义的req的结构体为:

typedef struct _match_desc_req_t

{

    U16 nwk_addr;       ///< Network address of target device

    U16 prof_id;        ///< Profile ID

    U8  *clust_list;  ///< Marker for start of cluster list

} match_desc_req_t;

此处将NumberInClustersInClusterList NumberOutClusters OutClusterList四个参数合并为一个U8的指针,即依次存放一个U8NumberInClusters,然后存放NumberInClustersInClusterListNumberOutClustersOutClusterList也是同理。

那么当设备收到match_desc_req_t这个信息时,就会与本身保存的profile ID进行匹配,匹配的数组如下:

static U8 test_data_simple_desc[] = {

    TEST_DATA_EP,               // ep

    0x00,                       // profile id

    0xC0,

    0xAA,                       // dev id

    0x33,                       // dev ver

    1,                          // num in clusters

    TEST_DATA_IN_CLUST,         // in clusters  

    1,                          // num out clusters

    TEST_DATA_OUT_CLUST         // out clusters

};

此处可以发现test_data_simple_desc里的参数都为U8类型的,所以需要和match_desc_req_t结构体里的参数做匹配,这里就涉及到字节对齐的问题了;也就是说为了使数据能够正确的传输,那么就需要对test_data_simple_desc里的U8对应的_match_desc_req_t里为U16的类型进行对齐处理(编译器实际处理中也是这样的),所以此时就需要对_match_desc_req_t里为U16test_data_simple_desc里为U8进行对齐操作(即补0),如下:

static U8 test_data_simple_desc[] = {

    TEST_DATA_EP,               // ep

    0x00,

    0x00,                      // profileid

    0xC0,

    0xAA,                       // dev id

    0x00,

    0x33,                       // dev ver

    1,                          // num in clusters

    TEST_DATA_IN_CLUST,         // in clusters  

    1,                          // num out clusters

    TEST_DATA_OUT_CLUST         // out clusters

};

上面的demo是字节对齐的一个典型问题,下面我们再来详细研究一下,到底什么是字节对齐呢? 我们现在的计算机中内存空间都是按照字节来进行划分的,从理论上来讲的话似乎对任何类型的变量的访问可以从任何地址开始,然而值得注意的就是,实际情况下在访问特定变量的时候经常在特定的内存地址访问,从而就需要各种类型的数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

  在此之前我们不得不提的一个操作符就是sizeof,其作用就是返回一个对象或者类型所占的内存字节数。我们为什么不在此称之为sizeof()函数呢?看看下面一段代码:

 #include

  void print()

  {

  printf("hello world!\n");

  return ;

  }

  void main()

  {

  printf("%d\n",sizeof(print()));

  return ;

  }

  这段代码在linux环境下我采用gcc编译是没有任何问题的,对于void类型,其长度为1,但是如果我们在vc6下面运行的话话就会出现illegal sizeof operand错误,所以我们称之为操作符更加的准确些,既然是操作符,那么我们来看看它的几种使用方式:

  1sizeof( object ); // sizeof( 对象 );

  2 sizeof( type_name ); // sizeof( 类型 );

  3sizeof object; // sizeof 对象; 通常这种写法我们在代码中都不会使用,所以很少见到。

  下面来看段代码加深下印象:

  #include

  void main()

  {

  int i;

  printf("sizeof(i):\t%d\n",sizeof(i));

  printf("sizeof(4):\t%d\n",sizeof(4));

  printf("sizeof(4+2.5):\t%d\n",sizeof(4+2.5));

  printf("sizeof(int):\t%d\n",sizeof(int));

  printf("sizeof 5:\t%d\n",sizeof 5);

  return ;

  }

  运行结果为:

 

  sizeof(i): 4

  sizeof(4): 4

  sizeof(4+2.5): 8

  sizeof(int): 4

  sizeof 5: 4

  从运行结果我们可以看出上面的几种使用方式,实际上,sizeof计算对象的大小也是转换成对对象类型的计算,也就是说,同种类型的不同对象其sizeof值都是一样的。从给出的代码中我们也可以看出sizeof可以对一个表达式求值,编译器根据表达式的最终结果类型来确定大小,但是一般不会对表达式进行计算或者当表达式为函数时并不执行函数体。如:

  #include

  int print()

  {

  printf("Hello bigloomy!");

  return 0;

  }

  void main()

  {

  printf("sizeof(print()):\t%d\n",sizeof(print()));

  return ;

  }

  运行结果为:

 

  sizeof(print()): 4

  从结果我们可以看出print()函数并没有被调用。

  接下来我们来看看linux内核链表里的一个宏:

  #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE*)0)->MEMBER)

  对这个宏的讲解我们大致可以分为以下4步进行讲解:

  1( (TYPE *)0 ) 0地址强制 "转换" TYPE结构类型的指针;

  2((TYPE *)0)->MEMBER 访问TYPE结构中的MEMBER数据成员;

  3&( ( (TYPE *)0 )->MEMBER)取出TYPE结构中的数据成员MEMBER的地址;

  4(size_t)(&(((TYPE*)0)->MEMBER))结果转换为size_t类型。

  宏offsetof的巧妙之处在于将0地址强制转换为 TYPE结构类型的指针,TYPE结构以内存空间首地址0作为起始地址,则成员地址自然为偏移地址。可能有的读者会想是不是非要用0?当然不是,我们仅仅是为了计算的简便。也可以使用是他的值,只是算出来的结果还要再减去该数值才是偏移地址。来看看下面的代码:

  #include

  #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE*)4)->MEMBER)

  typedef struct stu1

  {

  int a;

  int b;

  }stu1;

  void main()

  {

  printf("offsetof(stu1,a):\t%d\n",offsetof(stu1,a)-4);

  printf("offsetof(stu1,b):\t%d\n",offsetof(stu1,b)-4);

  }

  运行结果为:

 

 

  offsetof(stu1,a): 0

  offsetof(stu1,b): 4

  为了让读者加深印象,我们这里在代码中没有使用0,而是使用的4,所以在最终计算出的结果部分减去了一个4才是偏移地址,当然实际使用中我们都是用的是0

  懂了上面的宏offsetof之后我们再来看看下面的代码:

  #include

  #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

  typedef struct stu1

  {

  int a;

  char b[1];

  int c;

  }stu1;

  void main()

  {

  printf("offsetof(stu1,a):\t%d\n",offsetof(stu1,a));

  printf("offsetof(stu1,b):\t%d\n",offsetof(stu1,b));

  printf("offsetof(stu1,c):\t%d\n",offsetof(stu1,c));

  printf("sizeof(stu1) :\t%d\n",sizeof(stu1));

  }

  运行结果为:

 

    offsetof(stu1,a):0

  offsetof(stu1,b): 4

  offsetof(stu1,c): 8

  sizeof(stu1) : 12

  对于字节对齐不了解的读者可能有疑惑的是c的偏移量怎么会是8和结构体的大小怎么会是12?因该是sizeof(int)+sizeof(char)+sizeof(int)=9。其实这是编译器对变量存储的一个特殊处理。为了提高CPU的存储速度,编译器对一些变量的起始地址做了对齐处理。在默认情况下,编译器规定各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。现在来分析下上面的代码,如果我们假定a的起始地址为0,它占用了4个字节,那么接下来的空闲地址就是4,是1的倍数,满足要求,所以b存放的起始地址是4,占用一个字节。接下来的空闲地址为5,而cint变量,占用4个字节,5不是4的整数倍,所以向后移动,找到离5最近的8作为存放c的起始地址,c也占用4字节,所以最后使得结构体的大小为12。现在我们再来看看下面的代码:

  #include

  typedef struct stu1

  {

  char array[7];

  }stu1;

  typedef struct stu2

  {

  double fa;

  }stu2;

  typedef struct stu3

  {

  stu1 s;

  char str;

  }stu3;

  typedef struct stu4

  {

  stu2 s;

  char str;

  }stu4;

  void main()

  {

  printf("sizeof(stu1) :\t%d\n",sizeof(stu1));

  printf("sizeof(stu2) :\t%d\n",sizeof(stu2));

  printf("sizeof(stu3) :\t%d\n",sizeof(stu3));

  printf("sizeof(stu4) :\t%d\n",sizeof(stu4));

  }

  运行结果为:

 

 

  sizeof(stu1) : 7

  sizeof(stu2) : 8

  sizeof(stu3) : 8

  sizeof(stu4) : 16

  分析下上面我们的运行结果,重点是struct stu3struct stu4,在struct stu3中使用的是一个字节对齐,因为在stu1stu3中都只有一个char类型,在structstu3中我们定义了一个stu1类型的 s,而stu1所占的大小为7,所以加上加上接下来的一个字节strsizeof(stu3)8。在stu4中,由于我们定义了一个stu2类型的s,而s是一个double类型的变量,占用8字节,所以接下来在stu4中采用的是8字节对齐。如果我们此时假定stu4中的s从地址0开始存放,占用8个字节,接下来的空闲地址就是8,根据我们上面的讲解可知刚好可以在此存放str。所以变量都分配完空间后stu4结构体所占的字节数为9,但9不是结构体的边界数,也就是说我们要求分配的字节数为结构体中占用空间最大的类型所占用的字节数的整数倍,在这里也就是double类型所占用的字节数8的整数倍,所以接下来还要再分配7个字节的空间,该7个字节的空间没有使用,由编译器自动填充,没有存放任何有意义的东西。

  当然我们也可以使用预编译指令#pragma pack (value)来告诉编译器,使用我们指定的对齐值来取代缺省的。接下来我们来看看一段代码。

   #include

  #pragma pack (1) /*指定按1字节对齐*/

  typedef union stu1

  {

  char str[10];

  int b;

  }stu1;

  #pragma pack () /*取消指定对齐,恢复缺省对齐*/

  typedef union stu2

  {

  char str[10];

  int b;

  }stu2;

  void main()

  {

  printf("sizeof(stu1) :\t%d\n",sizeof(stu1));

  printf("sizeof(stu2) :\t%d\n",sizeof(stu2));

  }

  运行结果为:

 

 

  sizeof(stu1) : 10

  sizeof(stu2) : 12

  现在来分析下上面的代码。由于之前我们一直都在使用struct,所以在这里我们特地例举了一个union的代码来分析下,我们大家都知道union的大小取决于它所有的成员中占用空间最大的一个成员的大小。由于在unionstu1中我们使用了1字节对齐,所以对于stu1来说占用空间最大的是char str[10]类型的数组,,其值为10。为什么stu110stu2却是12?因为在stu2的上面我们使用了#pragma pack () ,取消指定对齐,恢复缺省对齐。所以由于stu2其中int类型成员的存在,使stu2的对齐方式变成4字节对齐,也就是说,stu2的大小必须在4的对界上,换句话说就是stu2的大小要是4的整数倍,所以占用的空间变成了12

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值