c语言输出占六个字节向右靠齐,C语言字节对齐问题详解

引言

考虑下面的结构体定义:html

1 typedef struct{2 charc1;3 shorts;4 charc2;5 inti;6 }T_FOO;

假设这个结构体的成员在内存中是紧凑排列的,且c1的起始地址是0,则s的地址就是1,c2的地址是3,i的地址是4。linux

如今,咱们编写一个简单的程序:面试

1 int main(void){2 T_FOO a;3 printf("c1 -> %d, s -> %d, c2 -> %d, i -> %d\n",4 (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,5 (unsigned int)(void*)&a.s - (unsigned int)(void*)&a,6 (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,7 (unsigned int)(void*)&a.i - (unsigned int)(void*)&a);8 return 0;9 }

运行后输出: 算法

1 c1 -> 0, s -> 2, c2 -> 4, i -> 8

为何会这样?这就是字节对齐致使的问题。编程

本文在参考诸多资料的基础上,详细介绍常见的字节对齐问题。因成文较早,资料来源大多已不可考,敬请谅解。数组

一  什么是字节对齐

现代计算机中,内存空间按照字节划分,理论上能够从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时常常在特定的内存地址访问,这就须要各类类型数据按照必定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。安全

二  对齐的缘由和做用

不一样硬件平台对存储空间的处理上存在很大的不一样。某些平台对特定类型的数据只能从特定地址开始存取,而不容许其在内存中任意存放。例如Motorola 68000 处理器不容许16位的字存放在奇地址,不然会触发异常,所以在这种架构下编程必须保证字节对齐。网络

但最多见的状况是,若是不按照平台要求对数据存放进行对齐,会带来存取效率上的损失。好比32位的Intel处理器经过总线访问(包括读和写)内存数据。每一个总线周期从偶地址开始访问32位内存数据,内存数据以字节为单位存放。若是一个32位的数据没有存放在4字节整除的内存地址处,那么处理器就须要2个总线周期对其进行访问,显然访问效率降低不少。数据结构

所以,经过合理的内存对齐能够提升访问效率。为使CPU可以对数据进行快速访问,数据的起始地址应具备“对齐”特性。好比4字节数据的起始地址应位于4字节边界上,即起始地址可以被4整除。架构

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

三  对齐的分类和准则

主要基于Intel X86架构介绍结构体对齐和栈内存对齐,位域本质上为结构体类型。

对于Intel X86平台,每次分配内存应该是从4的整数倍地址开始分配,不管是对结构体变量仍是简单类型的变量。

3.1 结构体对齐

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

字节对齐的问题主要就是针对结构体。

3.1.1 简单示例

先看个简单的例子(32位,X86处理器,GCC编译器):

【例1】设结构体以下定义:

1 structA{2 inta;3 charb;4 shortc;5 };6 structB{7 charb;8 inta;9 shortc;10 };

已知32位机器上各数据类型的长度为:char为1字节、short为2字节、int为4字节、long为4字节、float为4字节、double为8字节。那么上面两个结构体大小如何呢?

结果是:sizeof(strcut A)值为8;sizeof(struct B)的值倒是12。

结构体A中包含一个4字节的int数据,一个1字节char数据和一个2字节short数据;B也同样。按理说A和B大小应该都是7字节。之因此出现上述结果,就是由于编译器要对数据成员在空间上进行对齐。

3.1.2 对齐准则

先来看四个重要的基本概念:

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

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

3) 指定对齐值:#pragma pack (value)时的指定对齐值value。

4) 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。

基于上面这些值,就能够方便地讨论具体数据结构的成员和其自身的对齐方式。

其中,有效对齐值N是最终用来决定数据存放地址方式的值。有效对齐N表示“对齐在N上”,即该数据的“存放起始地址%N=0”。而数据结构中的数据变量都是按定义的前后顺序存放。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐存放,结构体自己也要根据自身的有效对齐值圆整(即结构体成员变量占用总长度为结构体有效对齐值的整数倍)。

以此分析3.1.1节中的结构体B:

假设B从地址空间0x0000开始存放,且指定对齐值默认为4(4字节对齐)。成员变量b的自身对齐值是1,比默认指定对齐值4小,因此其有效对齐值为1,其存放地址0x0000符合0x0000%1=0。成员变量a自身对齐值为4,因此有效对齐值也为4,只能存放在起始地址为0x0004~0x0007四个连续的字节空间中,符合0x0004%4=0且紧靠第一个变量。变量c自身对齐值为 2,因此有效对齐值也是2,可存放在0x0008~0x0009两个字节空间中,符合0x0008%2=0。因此从0x0000~0x0009存放的都是B内容。

再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)因此就是4,因此结构体的有效对齐值也是4。根据结构体圆整的要求, 0x0000~0x0009=10字节,(10+2)%4=0。因此0x0000A~0x000B也为结构体B所占用。故B从0x0000到0x000B 共有12个字节,sizeof(struct B)=12。

之因此编译器在后面补充2个字节,是为了实现结构数组的存取效率。试想若是定义一个结构B的数组,那么第一个结构起始地址是0没有问题,可是第二个结构呢?按照数组的定义,数组中全部元素都紧挨着。若是咱们不把结构体大小补充为4的整数倍,那么下一个结构的起始地址将是0x0000A,这显然不能知足结构的地址对齐。所以要把结构体补充成有效对齐大小的整数倍。其实对于char/short/int/float/double等已有类型的自身对齐值也是基于数组考虑的,只是由于这些类型的长度已知,因此他们的自身对齐值也就已知。

上面的概念很是便于理解,不过我的仍是更喜欢下面的对齐准则。

结构体字节对齐的细节和具体编译器实现相关,但通常而言知足三个准则:

1) 结构体变量的首地址可以被其最宽基本类型成员的大小所整除;

2) 结构体每一个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,若有须要编译器会在成员之间加上填充字节(internal adding);

3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,若有须要编译器会在最末一个成员以后加上填充字节{trailing padding}。

对于以上规则的说明以下:

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

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

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

【例2】假设4字节对齐,如下程序的输出结果是多少?

1 /*OFFSET宏定义可取得指定结构体某成员在结构体内部的偏移*/

2 #define OFFSET(st, field) (size_t)&(((st*)0)->field)

3 typedef struct{4 chara;5 shortb;6 charc;7 intd;8 char e[3];9 }T_Test;10

11 int main(void){12 printf("Size = %d\n a-%d, b-%d, c-%d, d-%d\n e[0]-%d, e[1]-%d, e[2]-%d\n",13 sizeof(T_Test), OFFSET(T_Test, a), OFFSET(T_Test, b),14 OFFSET(T_Test, c), OFFSET(T_Test, d), OFFSET(T_Test, e[0]),15 OFFSET(T_Test, e[1]),OFFSET(T_Test, e[2]));16 return 0;17 }

执行后输出以下:

1 Size = 16

2 a-0, b-2, c-4, d-8

3 e[0]-12, e[1]-13, e[2]-14

下面来具体分析:

首先char a占用1个字节,没问题。

short b自己占用2个字节,根据上面准则2,须要在b和a之间填充1个字节。

char c占用1个字节,没问题。

int d自己占用4个字节,根据准则2,须要在d和c之间填充3个字节。

char e[3];自己占用3个字节,根据原则3,须要在其后补充1个字节。

所以,sizeof(T_Test) = 1 + 1 + 2 + 1 + 3 + 4 + 3 + 1 = 16字节。

3.1.3 对齐的隐患

3.1.3.1 数据类型转换

代码中关于对齐的隐患,不少是隐式的。例如,在强制类型转换的时候:

1 int main(void){2 unsigned int i = 0x12345678;3

4 unsigned char *p = (unsigned char *)&i;5 *p = 0x00;6 unsigned short *p1 = (unsigned short *)(p+1);7 *p1 = 0x0000;8

9 return 0;10 }

最后两句代码,从奇数边界去访问unsigned short型变量,显然不符合对齐的规定。在X86上,相似的操做只会影响效率;但在MIPS或者SPARC上可能致使error&

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值