大家好,欢迎来到IT知识分享网。
今天重新测试了一下结构体中字节对齐的问题,正好利用 SEGGER RTT 的 LOG 系统打印出地址来印证一下。
今天主要探讨三个部分:
- 什么是结构体中的字节对齐现象
- 嵌套的结构体中,字节是如何对齐的
- 字节对齐的本质是什么
一、什么是结构体的字节对齐现象
程序员,咱都用代码说话,先上 code:
(说明:以下代码均在 ARM 平台上,使用 Keil 进行编译测试)
#define offset_of(TYPE, MEMBER) ((size_t) &((TYPE *)0)-MEMBER) //上面这个宏定义主要用于显示结构体成员变量相对结构体起始地址的偏移 typedef struct stu1 { int a; char b; int c; }stu1; void main() { LOG_INFO("\r\n\r\n====== Struct Test ======\r\n\r\n"); LOG_INFO("offset_of(stu1,a):\t%d\n",offset_of(stu1,a)); LOG_INFO("offset_of(stu1,b):\t%d\n",offset_of(stu1,b)); LOG_INFO("offset_of(stu1,c):\t%d\n",offset_of(stu1,c)); LOG_INFO("sizeof(stu1) :\t%d\n",sizeof(stu1)); return ; }
对于上面的运行结果,对字节对齐不了解的同学可能会疑惑,c的偏移量怎么会是8呢?不应该是 5 吗?
结构体的大小怎么会是12呢?不应该是 9 吗?
不了解的同学可能会这样理解:
c的偏移量是sizeof(int)+sizeof(char) = 5
结构体stu1占用的内存大小应该是sizeof(int)+sizeof(char)+sizeof(int)=9。
通过下图所示的stu1的内存结构可以知道,编译器对变量存储进行了一个特殊处理。
为了提高CPU的存储速度,编译器对一些变量的起始地址做了对齐处理。
在默认情况下,编译器规定各成员变量存放的起始地址相对于结构体的起始地址的偏移量,必须为该变量的类型所占用的字节数和编译器编译过程中采用的字节对齐数两者中最小值的整数倍。
有点绕,比如stu1 结构体中,变量 c 类型为 int,也就是占用 4 字节,编译器采用 4 字节对齐,因此偏移量必须是 4 的整数倍。
typedef struct stu2 { int a; char b; char c int d; }stu2;
再比上面的 stu2中,如对于变量 c,其类型为 char ,占用 1 字节,编译器采用 4 字节对齐,因此 它被分配的偏移量需要是 1 的整数倍,在上面的结构体 stu2 中,c 的偏移量为 5。
如图:
现在来分析前面的代码
假定a的起始地址为0,它占用了4字节,接下来的空闲地址就是4,是1的倍数,满足要求,所以b存放的起始地址是4,占用一个字节,接下来的空闲地址为5。c也是char变量,占用1字节, 因此可以放在地址 5 上面。
接下来看地址 6,对于 d,它占用了 4 个字节,同时需要注意的是,编译器默认按照结构体中占有内存最大的类型所占用的字节数进行字节对齐。在此结构体中占用内存最大的为整型,占用4字节,所以在此取两者的最小值4,6 并不是4的整数倍,所以向后移动,找到离6最近的8作为存放d的起始地址,d也占用4字节,最后结构体的大小为12。
需要注意的就是,变量b和 c后面2字节的存储空间是由编译器自动填充的,其中没有存储任何有用的信息。
二、嵌套的结构体,字节又是如何对齐的呢
先来看下面的代码
typedef struct stu1 { char ary[5]; int a; }stu1; typedef struct stu2 { double a; char b; }stu2; typedef struct stu3 { stu1 s; char str; }stu3; typedef struct stu4 { stu2 s; char str; }stu4; LOG_INFO("\r\n\r\n====== Struct Test ======\r\n\r\n"); LOG_INFO("sizeof(stu1) :\t%d\n",sizeof(stu1)); LOG_INFO("sizeof(stu2) :\t%d\n",sizeof(stu2)); LOG_INFO("sizeof(stu3) :\t%d\n",sizeof(stu3)); LOG_INFO("sizeof(stu4) :\t%d\n",sizeof(stu4));
在上面的运行结果中,stu1和stu2所占内存的分别为12字节和16字节,对这两者的分析与前面相同。
我们重点看一下这里的stu3和stu4。
在默认情况下,结构体采用该结构体中占用内存最大的类型所占的字节数作为字节对齐方式,但是在stu3中定义的stu1结构体类型的变量s占用16字节,而stu3并不是按照16字节进行对齐的,而是采用4字节对齐,这是因为stu1和stu3中占用内存最大的是int型变量,占用4字节。因此在分析结构体字节对齐方式时需要将结构体分解为“原子类型”,如int、double、char、float、short等,而不是自定义的结构体类型。
找出分解出来的“原子类型”中占用内存最大的类型,将其占用的内存值作为结构体的默认字节对齐值。
在stu4中定义了stu2类型的结构体变量s,按照上面的方法先对stu2进行分解。分解出来的类型有double、char,stu4中还有char类型,其中占用内存最大的是double类型,占用内存大小为8字节,由此可知,stu4采用8字节对齐。
由于stu4中的stu2结构体类型变量s所占用的内存大小为16,而接下来定义了一个char类型的str变量,其偏移地址为16,占用一个字节,此时stu4占用的内存大小为17,不是字节对齐数8的整数倍,所以在stu4占用的内存的最后添加7字节的空间,使其占有内存大小为24。
需要注意,编译器添加的内存并没有使用,没有存放任何有意义的内容。
在结构体的嵌套中,不管遇到多少层的嵌套,都可以按照这种分解方法,对结构体进行逐层分解,再根据分解出来的“原子类型”分析结构体的字节对齐方式.
看下面的例子,会更清楚一些
typedef struct stu2 { char a; short c; int d; int b; }stu2; typedef struct stu4 { stu2 s; char str; double h; }stu4; LOG_INFO("offset_of(stu4,s):\t%d\n",offset_of(stu4,s)); LOG_INFO("offset_of(stu4,str):\t%d\n",offset_of(stu4,str)); LOG_INFO("offset_of(stu4,h):\t%d\n",offset_of(stu4,h));
在 stu2 中,a 的偏移地址为 0,c 的偏移地址为 2,d 的偏移地址为 4,b 的偏移地址为 8。这里的变量 a,c,d 组成第一个对齐单元,变量 b 会和 stu4 中的 str 组合成一个对齐单元。
套在 stu4 中以后,str 的起始地址就为 12,这里,stu2 的 b 和 stu4 的 str 共同组成了第二个 8 字节的对齐单元。
最后一个对齐单元是 double 类型的 h 变量。
三、为什么要做字节对齐呢
其实主要是为了效率,也就是处理器从内存中读取数据的时候,可以使用尽量少的操作,从而读取到完整的数据类型,我用硬件底层来说明一下这个道理,我们暂时以 4 字节对齐为例。
我们知道在内存的总线系统中,地址线其实都是从 A2 开始的,A0和 A1 一般都悬空,这是因为Arm 系统中硬件地址访问也是按照 4 字节对齐访问的。
这样我们可以通过地址 0,4,8 这样的地址读到 int 类型的数据,假如我们在结构体中强制按照 1 字节对齐,这个时候很可能出现一个 int 类型的起始地址不在 0,4,8这样的对齐地址上,而是放在了 地址 5 上,那么我们的硬件要是读取这个变量,则需要先读取 地址4,再读取一次 地址 8。然后通过左右移位才能组合出一个完整的变量,这里的多次读取就涉及到了效率问题了。
这里是从底层的硬件接口剖析,到软件上层的处理,道理也是一样的。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/95429.html