字节对齐_大小端_位序

字节序

字节序,顾名思义就是字节的高低位存放顺序。1

对于单字节,大部分处理器以相同的顺序处理比特位,因此单字节的存放和传输方式一般相同。

对于多字节数据,如整型(32位机中一般占4字节),在不同的处理器的存放方式主要有两种(以内存中0x0A0B0C0D的存放方式为例)。

大端,Big-Endian

在计算机中,存储介质以下面方式存储整数0x0A0B0C0D则称为大字节序:

0x0A0xOB0xOC0xOD
0x00000x00010x00020x0003

其中,最高有效位(MSB,Most Significant Byte)0x0A存储在最低的内存地址处。下个字节0x0B存在后面的地址处。同时,最高的16bit单元0x0A0B存储在低位。

小端,Littel-Endian

在计算机中,存储介质以下面方式存储整数0x0A0B0C0D则称为小字节序:

0x0D0xOC0xOB0xOA
0x00000x00010x00020x0003

其中,最低有效位(LSB,Least Significant Byte)0x0D存储在最低的内存地址处。

网络序

网络传输一般采用大字节序,也称为网络字节序或网络序。IP协议中定义大字节序为网络字节序。

字节对齐

what?

现代计算机系统结构中,数据在内存中是按字节存放的,但访问不同类型的数据时,需使用特定的内存地址更新模式。
字节对齐即对数据在内存中的存储位置进行调整,使其为该数据类型大小的整数倍,方便数据读取,提高数据读取效率。

why?

char A;
int B;

若上述变量分配存储空间时,不考虑字节对齐,在内存中按以下方式存储

-0x000x010x020x030x040x050x060x070x080x090x0A0x0B0x0C0x0D0x0E0x0F
0x10AB_01B_02B_03B_04
0x20
0x30

假设计算机系统字长为4个字节,内存地址从0x00开始分配,字符型变量A占用1个字节,整形变量B占用4个字节(按B_01…B_04存储,不考虑大小端)。

所以在处理变量A与B时的过程可能大致为:

A:将0x00-0x03共32位读入寄存器,再通过左移24位再右移24位运算得到a的值(或与0x000000FF做与运算)

B:将0x00-0x03这32位读入寄存器,通过位运算得到低24位的值;再将0x04-0x07这32位读入寄存器,通过位运算得到高8位的值;再与最先得到的24位做位运算,才可得到整个32位的值。

上面叙述可知,对A的处理是最简处理,可对B的处理,本身是个32位数,处理的时候却得折成2部分,之后再合并,效率上就有些低了。

想解决这个问题,就需要付出几个字节浪费的代价,改为下图的分配方式:
若上述变量分配存储空间时,不考虑字节对齐,在内存中按以下方式存储

-0x000x010x020x030x040x050x060x070x080x090x0A0x0B0x0C0x0D0x0E0x0F
0x10A***B_01B_02B_03B_04
0x20
0x30

注:* 代表填充字节
按上面的分配方式,A的处理过程不变;B却简单得多了:只需将0x04-0x07这32位读入寄存器就OK了。
但最常见的情况是,如果不按照平台要求对数据存放进行对齐,会带来存取效率上的损失。比如32位的Intel处理器通过总线访问(包括读和写)内存数据。每个总线周期从偶地址开始访问32位内存数据,内存数据以字节为单位存放。如果一个32位的数据没有存放在4字节整除的内存地址处,那么处理器就需要2个总线周期对其进行访问,显然访问效率下降很多。

因此,通过合理的内存对齐可以提高访问效率。为使CPU能够对数据进行快速访问,数据的起始地址应具有“对齐”特性。比如4字节数据的起始地址应位于4字节边界上,即起始地址能够被4整除。

此外,合理利用字节对齐还可以有效地节省存储空间。但要注意,在32位机中使用1字节或2字节对齐,反而会降低变量访问速度。因此需要考虑处理器类型。还应考虑编译器的类型。在VC/C++和GNU GCC中都是默认是4字节对齐。

How?

在C语言中,结构体是种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构体、联合等)的数据单元。编译器为结构体的每个成员按照其自然边界(alignment)分配空间。各成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。

相关概念

  • 数据类型自身的对齐值
    char型数据自身对齐值为1字节,short型数据为2字节,int/float型为4字节,double型为8字节。

  • 结构体或类的自身对齐值
    其成员中自身对齐值最大的那个值。

  • 指定对齐值
    编译配置文件中,#pragma pack (value)时的指定对齐值value。

  • 数据成员、结构体和类的有效对齐值
    自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack}
    ###总体要求
    结构体内成员变量要对齐存放;
    结构体元素本身也要根据自身对齐值对齐存放(即结构体成员变量总长度为结构体有效对齐值的整数倍)
    ###对齐规则

  • 结构体中数据元素按照定义顺序依次置于内存中,从结构体首地址开始依次将元素放入内存时,元素会被放置在其自身对齐大小的整数倍地址上。

  • 结构体内数据元素对齐完成后,如果结构体大小不是所有元素中最大数据类型对齐大小的整数倍,则结构体对齐到最大元素对齐大小的整数倍,填充空间放置到结构体末尾。

备注:

第一条:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。

第二条:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员大小的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。

第三条:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。

/* OFFSET宏定义可取得指定结构体某成员在结构体内部的偏移 */
 #define OFFSET(st, field)     (size_t)&(((st*)0)->field)
 typedef struct
 {
     char  a; /*占用1个字节*/
     short b; /*非自身对齐值整数倍,需填充,填充1B+2B,填充一个字节*/
     char  c; /*占用1B,无需填充*/
     int   d; /*非自身对齐值整数倍,需填充3B, 3B + 4B*/
     char  e[3]; /*不需填充,3B*/
 }T_Test; /*结构体内数据元素对齐大小(15B)非整形数据大小整数倍,需填充1B*/
  /*T_TEST Size = 1B + (1B+2B) + (1B)+ (3B+4B) + (3B)  + (1B)*/

int main(void)
{  
     printf("Size = %d\n  a-%d, b-%d, c-%d, d-%d\n  e[0]-%d, e[1]-%d, e[2]-%d\n",
            sizeof(T_Test), OFFSET(T_Test, a), OFFSET(T_Test, b),
            OFFSET(T_Test, c), OFFSET(T_Test, d), OFFSET(T_Test, e[0]),
            OFFSET(T_Test, e[1]),OFFSET(T_Test, e[2]));
     return 0;
}
执行结果:
 Size = 16
 a-0, b-2, c-4, d-8
 e[0]-12, e[1]-13, e[2]-14

相关面试题

Intel/微软C语言面试题

1 #pragma pack(8)
 2 struct s1{
 3     short a;
 4     long  b;
 5 };
 6 struct s2{
 7     char c;
 8     s1   d;
 9     long long e;  //VC6.0下可能要用__int64代替双long
10 };
11 #pragma pack()
  • 问:1. sizeof(s2) = ? 2. s2的s1中的a后面空了几个字节接着是b?

  • 分析

成员对齐有一个重要的条件,即每个成员分别按自己的方式对齐。

也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐。其对齐的规则是:每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8字节)中较小的一个对齐,并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。

s1中成员a是1字节,默认按1字节对齐,而指定对齐参数为8,两值中取1,即a按1字节对齐;成员b是4个字节,默认按4字节对齐,这时就按4字节对齐,所以sizeof(s1)应该为8;

s2中c和s1中a一样,按1字节对齐。而d 是个8字节结构体,其默认对齐方式就是所有成员使用的对齐参数中最大的一个,s1的就是4。所以,成员d按4字节对齐。成员e是8个字节,默认按8字节对齐,和指定的一样,所以它对到8字节的边界上。这时,已经使用了12个字节,所以又添加4个字节的空,从第16个字节开始放置成员e。此时长度为24,并可被8(成员e按8字节对齐)整除。这样,一共使用了24个字节。

各个变量在内存中的布局为:

c***aa**

bbbb****

dddddddd     ——这种“矩阵写法”很方便看出结构体实际大小!

因此,sizeof(S2)结果为24,a后面空了2个字节接着是b。

这里有三点很重要:

  1. 每个成员分别按自己的方式对齐,并能最小化长度;

  2. 复杂类型(如结构)的默认对齐方式是其最长的成员的对齐方式,这样在成员是复杂类型时可以最小化长度;

  3. 对齐后的长度必须是成员中最大对齐参数的整数倍,这样在处理数组时可保证每一项都边界对齐。

还要注意,“空结构体”(不含数据成员)的大小为1,而不是0。试想如果不占空间的话,一个空结构体变量如何取地址、两个不同的空结构体变量又如何得以区分呢?

上海网宿科技面试题

 假设硬件平台是intel x86(little endian),以下程序输出什么:
 1 //假设硬件平台是intel x86(little endian)
 2 typedef unsigned int uint32_t;
 3 void inet_ntoa(uint32_t in){
 4     char  b[18];
 5     register  char  *p;
 6     p = (char *)∈
 7 #define UC(b) (((int)b)&0xff) //byte转换为无符号int型
 8     sprintf(b, "%d.%d.%d.%d\n", UC(p[0]), UC(p[1]), UC(p[2]), UC(p[3]));
 9     printf(b);
10 }
11 int main(void){  
12     inet_ntoa(0x12345678);
13     inet_ntoa(0x87654321);
14     return 0;
15 }

先看如下程序:

1 int main(void){  
2     int a = 0x12345678;
3     char *p = (char *)&a;
4     char str[20];
5     sprintf(str,"%d.%d.%d.%d\n", p[0], p[1], p[2], p[3]);
6     printf(str);
7     return 0;
8 }

按照小字节序的规则,变量a在计算机中存储方式为:

  • 小端序
0x780x560x340x12
0x00000x00010x00020x0003

注意,p并不是指向0x12345678的开头0x12,而是指向0x78。p[0]到p[1]的操作是&p[0]+1,因此p[1]地址比p[0]地址大。输出结果为120.86.52.18。

反过来的话,令int a = 0x87654321,则输出结果为33.67.101.-121。

为什么有负值呢?因为系统默认的char是有符号的,本来是0x87也就是135,大于127因此就减去256得到-121。

想要得到正值的话只需将

char *p = (char *)&a改为unsigned char *p = (unsigned char *)&a即可。

综上不难得出,网宿面试题的答案为120.86.52.18和33.67.101.135。

位序

C语言中的位域结构也要遵循比特序(类似字节序)。例如:

1 struct bitfield{
2     unsigned char a: 2;
3     unsigned char b: 6;
4 }

该位域结构占1个字节,假设赋值a = 0x01和b=0x02,则大字节机器上该字节为(01)(000010),小字节机器上该字节为(000010)(01)。因此在编写可移植代码时,需要加条件编译。

注意,在包含位域的C结构中,若位域A在位域B之前定义,则位域A所占用的内存空间地址低于位域B所占用61的内存空间。

Bit order usually follows the same endianness as the byte order for a given computer system. That is, in a big endian system the most significant bit is stored at the lowest bit address; in a little endian system, the least significant bit is stored at the lowest bit address.2

位域对齐

位域定义

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1两种状态,用一位二进位即可。为了节省存储空间和处理简便,C语言提供了一种数据结构,称为“位域”或“位段”。

位域是一种特殊的结构成员或联合成员(即只能用在结构或联合中),用于指定该成员在内存存储时所占用的位数,从而在机器内更紧凑地表示数据。每个位域有一个域名,允许在程序中按域名操作对应的位。这样就可用一个字节的二进制位域来表示几个不同的对象。
位域定义与结构定义类似,其形式为:

  • 定义

struct 位域结构名
{
位域列表
};

其中位域列表的形式为:

类型说明符位域名:位域长度

  • 位域的使用

位域的使用和结构成员的使用相同,其一般形式为:

位域变量名.位域名

  • 位域的输出

位域允许用各种格式输出。

  • 位域变量的使用
    位域在本质上就是一种结构类型,不过其成员是按二进位分配的。位域变量的说明与结构变量说明的方式相同,可先定义后说明、同时定义说明或直接说明。

  • 位域的适用场景

    • 当机器可用内存空间较少而使用位域可大量节省内存时。如把结构作为大数组的元素时。

    • 当需要把一结构体或联合映射成某预定的组织结构时。如需要访问字节内的特定位时。

对齐准则

位域成员不能单独被取sizeof值。下面主要讨论含有位域的结构体的sizeof。

C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型的存在。位域作为嵌入式系统中非常常见的一种编程工具,优点在于压缩程序的存储空间。

其对齐规则大致为:

  • 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;

  • 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;

  • 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++和GCC采取压缩方式;

  • 如果位域字段之间穿插着非位域字段,则不进行压缩;

  • 整个结构体的总大小为最宽基本类型成员大小的整数倍,而位域则按照其最宽类型字节数对齐。

 struct BitField
 {
     char element1  : 1;
     char element2  : 4;
     char element3  : 5;
 };
位域类型为char,第1个字节仅能容纳下element1和element2,所以element1和element2被压缩到第1个字节中,而element3只能从下一个字节开始。因此sizeof(BitField)的结果为2
 struct BitField1
 {
     char element1   : 1;
     short element2  : 5;
     char element3   : 7;
 };
由于相邻位域类型不同,在VC6中其sizeof为6,在Dev-C++中为2
struct BitField2
{
     char element1  : 3;
     char element2  ;
     char element3  : 5;
};
非位域字段穿插在其中,不会产生压缩,在VC6和Dev-C++中得到的大小均为3。
 struct StructBitField
 {
     int element1   : 1;
     int element2   : 5;
     int element3   : 29;
     int element4   : 6;
     char element5  :2;
     char stelement;  //在含位域的结构或联合中也可同时说明普通成员
 };
 位域中最宽类型int的字节数为4,因此结构体按4字节对齐,在VC6中其sizeof为16。

注意事项

关于位域操作有几点需要注意:

  • 位域的地址不能访问,因此不允许将&运算符用于位域。不能使用指向位域的指针也不能使用位域的数组(数组是种特殊指针)。

    例如,scanf函数无法直接向位域中存储数据:

1 int main(void){  
2     struct BitField1 tBit;
3     scanf("%d", &tBit.element2); //error: cannot take address of bit-field 'element2'
4     return 0;
5 }

可用scanf函数将输入读入到一个普通的整型变量中,然后再赋值给tBit.element2。

  • 位域不能作为函数返回的结果。

  • 位域以定义的类型为单位,且位域的长度不能够超过所定义类型的长度。例如定义int a:33是不允许的。

  • 位域可以不指定位域名,但不能访问无名的位域。

位域可以无位域名,只用作填充或调整位置,占位大小取决于该类型。例如,char :0表示整个位域向后推一个字节,即该无名位域后的下一个位域从下一个字节开始存放,同理short :0和int :0分别表示整个位域向后推两个和四个字节。

当空位域的长度为具体数值N时(如int :2),该变量仅用来占位N位。


1 struct BitField3{
2     char element1  : 3;
3     char  :6;
4     char element3  : 5;
5 };
结构体大小为3。因为element1占3位,后面要保留6位而char为8位,所以保留的6位只能放到第2个字节。同样element3只能放到第3字节。
1 struct BitField4{
2     char element1  : 3;
3     char  :0;
4     char element3  : 5;
5 };
长度为0的位域告诉编译器将下一个位域放在一个存储单元的起始位置。如上,编译器会给成员element1分配3位,接着跳过余下的4位到下一个存储单元,然后给成员element3分配5位。故上面的结构体大小为2。
  • 位域的表示范围。

    • 位域的赋值不能超过其可以表示的范围;
    • 位域的类型决定该编码能表示的值的结果。

对于第二点,若位域为unsigned类型,则直接转化为正数;若非unsigned类型,则先判断最高位是否为1,若为1表示补码,则对其除符号位外的所有位取反再加一得到最后的结果数据(原码)。如:

1 unsigned int p:3 = 111;   //p表示7
2 int p:3 = 111;            //p 表示-1,对除符号位之外的所有位取反再加一
  • 带位域的结构在内存中各个位域的存储方式取决于编译器,既可从左到右也可从右到左存储。
int main(void){  
  union{
      int i;
      struct{
          char a : 1;
          char b : 1;
          char c : 2;
      }bits;
  }num;

  printf("Input an integer for i(0~15): ");
  scanf("%d", &num.i);
  printf("i = %d, cba = %d %d %d\n", num.i, num.bits.c, num.bits.b, num.bits.a);
  return 0;
}

输入i值为11,则输出为i = 11, cba = -2 -1 -1。

Intel x86处理器按小字节序存储数据,所以bits中的位域在内存中放置顺序为ccba。当num.i置为11时,bits的最低有效位(即位域a)的值为1,a、b、c按低地址到高地址分别存储为10、1、1(二进制)。

但为什么最后的打印结果是a=-1而不是1?

因为位域a定义的类型signed char是有符号数,所以尽管a只有1位,仍要进行符号扩展。1做为补码存在,对应原码-1。

如果将a、b、c的类型定义为unsigned char,即可得到cba = 2 1 1。1011即为11的二进制数。

注:C语言中,不同的成员使用共同的存储区域的数据构造类型称为联合(或共用体)。联合占用空间的大小取决于类型长度最大的成员。联合在定义、说明和使用形式上与结构体相似。

  • 位域的实现会因编译器的不同而不同,使用位域会影响程序可移植性。因此除非必要否则最好不要使用位域。

  • 尽管使用位域可以节省内存空间,但却增加了处理时间。当访问各个位域成员时,需要把位域从它所在的字中分解出来或反过来把一值压缩存到位域所在的字位中。


  1. https://www.cnblogs.com/clover-toeic/p/3853132.html ↩︎

  2. https://www.linuxjournal.com/article/6788 ↩︎

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值