目录
什么是字节对齐
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个字节的排放,这就是对齐。
为什么要字节对齐
各个硬件平台对存储空间的处理上有很大的不同。
一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。
比如有些平台每次读都是从偶地址开始,如果一个int型(假设为 32 位系统)存放在偶地址开始的地方,那么一个读周期就可以读出;而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。
基本类型及其大小
32位机器上各个数据类型所占的存储空间如下:
char:8位
short:16位
int:32位
long:32位
unsigned long:32位
long long:64位
float:32位
double:64位
long double:64位
指针:32位
64位机器上各个数据类型所占的存储空间如下:
char:8位
short:16位
int:32位
long:64位
unsigned long:64位
long long:64位
float:32位
double:64位
long double:128位
指针:64位
linux 下查看机器位数:
- ls /
#如果有lib64或这个目录,那操作系统就是64位的
- getconfig LONG_BIT
若输出32即为32位系统,64为64位系统
32位的系统中int类型和long类型一般都是4字节,
64位的系统中int类型还是4字节的,但是long已变成了8字节。
Linux系统中可 用"getconf WORD_BIT"和"getconf LONG_BIT"获得word和long的位数。64位系统中应该分别得到32和64。
- file查看可执行程序信息
# which ls alias ls='ls --color=auto' /bin/ls # file /bin/ls /bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=aaf05615b6c91d3cbb076af81aeff531c5d7dfd9, stripped
- uname -a
若为X86示意为64位系统,i386等位32位系统
结构体大小
计算结构体大小的原则
字节对齐的细节和编译器实现相关,但一般而言,满足以下准则:
- 结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
基本类型是指前面提到的像char、short、int、float、double,指针等等这样的内置数据类型。
如果一个结构体中包含另外一个结构体成员,那么此时最宽基本类型成员不是该结构体成员,而是结构体成员不断展开后,取基本类型的最宽值。
- 结构体每个成员相对于结构体首地址的偏移量(offset),都是成员大小的整数倍;
如有需要编译器会在成员之间加上填充字节。
- 结构体的总大小为结构体最宽基本类型成员大小的整数倍;
如有需要编译器会在最末一个成员之后加上填充字节。
偏移量
- 偏移量定义
成员偏移量指的是结构体变量中成员的地址和结构体变量地址的差。
注:结构体大小等于最后一个成员的偏移量加上最后一个成员的大小。
第一个结构体成员的偏移量就是0,所以结构体变量中第一个成员的地址就是结构体变量的首地址。
- 偏移量原则
- 结构体变量中成员的偏移量必须是该成员展开后最大基本类型大小的整数倍
(0被认为是任何数的整数倍)- 结构体大小必须是所有基本类型成员大小的整数倍,也即所有基本类型成员大小的公倍数。
- 范例
struct stru1
{
int a; //start address is 0
char b; //start address is 4
int c; //start address is 8
};
用sizeof求该结构体的大小,发现值为12。int占4个字节,char占1个字节,结果应该是9个字节才对啊.
为什么呢?
这个例子中前两个成员的偏移量都满足要求,但第三个成员的偏移量为5,并不是自身(int)大小的整数倍。
编译器在处理时会在第二个成员后面补上3个空字节,使得第三个成员的偏移量变成8。
结构体大小等于最后一个成员的偏移量加上其大小,上面的例子中计算出来的大小为12;
struct stru3
{
char i; //start address is 0
int m; //start address is 4
char n; //start address is 8
};
struct stru4
{
char i; //start address is 0
char n; //start address is 1
int m; //start address is 4
};
虽然结构体stru3和stru4中成员都一样,
但sizeof(struct stru3)的值为12而sizeof(struct stru4)的值为8。
查看结构体成员的偏移量
联合体的大小
- 联合体特征
- 联合体所有成员变量共享内存;
各个成员相对于联合体首地址偏移量都为0;
- 同一时间只能存储1个被选择的变量,对其他成员变量赋值会覆盖原变量
- 联合体大小计算规则
- 联合体大小要至少能容纳最大的成员变量;
注:
此时如果联合体中成员是数组,或者结构体,则比较的是数组成员/结构体成员的大小,找出最大值;
- 联合体大小要是所有成员变量展开后基本类型最大值的整数倍(注意是类型而不是变量)
- 范例
范例一
typedef union u
{
char a;
int b[5];
double c;
int d[3];
}U;
U大小至少要容纳最大的b[5]=4*5=20字节;
同时要是变量类型最大值得整数倍,即sizeof(double)=8的整数倍;所有sizeof(U)=24;
范例二:
typedef union u
{
char a;
int b[5];
double c;
int d[3];
}U;
struct stru1
{
int a; //start address is 0
char b; //start address is 4
int c; //start address is 8
};
struct stru3
{
char i; //start address is 0
int m; //start address is 4
char n; //start address is 8
};
typedef union u2
{
U uu;
double d[3];
struct stru1 s1;
struct stru3 s2_s[3];
}U2;
结果:
sizeof(U)=24.
sizeof(U2)=40.
结构体中嵌套结构体
- 原则
对于嵌套的结构体,需要将其展开。对结构体求sizeof时,上述两种原则变为:
- 展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大基本类型成员的整数倍。
- 结构体大小必须是所有基本类型成员大小的整数倍,这里所有成员计算的是展开后的基本类型成员,而不是将嵌套的结构体当做一个整体。
- 范例
struct stru5
{
short i;
struct
{
char c;
int j;
} tt;
int k;
};
结构体stru5的成员tt.c的偏移量应该是4,而不是2。整个结构体大小应该是16。
struct stru6
{
char i;
struct
{
char c;
int j;
} tt;
char a;
char b;
char d;
char e;
int f;
};
结构体tt单独计算占用空间为8,而stru6的sizeof则是20,不是8的整数倍;
这说明在计算sizeof(stru6)时,将嵌套的结构体ss展开了,
这样stu6中最大的成员为tt.j,占用4个字节,20为4的整数倍。
结构体中嵌套联合体
- 原则
由于联合体成员的偏移量会影响前一个成员后是否有填充,进而影响整个结构体的大小;所以优先看联合体成员的偏移量,再看联合体成员的大小;
- 联合体成员的偏移量:
- 将联合体成员展开;
- 联合体成员的偏移地址为其包含的最大基本类型的整数倍;
- 联合体成员的大小:
包含最大的成员&&是最大基本类型的整数倍;详细参见上诉联合体的规则;
- 范例
typedef struct
{
char a;
union
{
char a[10];
int b[2];
double c;
}test;
char d[5];
int e;
double f;
}Test;
测试:
Test test1;
printf("sizeof(Test)=%d, sizeof(test1.test)=%d, offset of test = %p, offset of e = %p, offset of f = %p.\n",
sizeof(Test), sizeof(test1.test), &((Test *)0)->test, &((Test *)0)->e, &((Test *)0)->f);
结果为:
sizeof(Test)=48, sizeof(test1.test)=16;
offset of test = 0x8, offset of e = 0x20, offset of f = 0x28.
结构体中包含数组
结构体中包含数组,其sizeof应当和处理嵌套结构体一样,将其展开;
- 范例
struct array
{
float f;
char p;
int arr[3];
};
sizeof(array),其值为20。
float占4个字节,到char p时偏移量为4,p占一个字节;
到int arr[3]时偏移量为5,扩展为int的整数倍,而非int arr[3]的整数倍,
这样偏移量变为8,而不是12。
结果是8+12=20,是最大成员float或int的大小的整数倍。
自定义n字节对齐
pragma pack
#Pragma是预处理指令,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作。
通过预编译指令#pragma pack (value)来告诉编译器,使用我们指定的对齐值来取代缺省的。
#pragma pack (2) /*指定按2字节对齐*/
struct C
{
char b;
int a;
short c;
}
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
结果:sizeof(struct C)值是8。
#pragma pack (1) /*指定按1字节对齐*/
struct D
{
char b;
int a;
short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
结果:sizeof(struct D)值为7。
基本概念
- 数据类型自身的对齐值:基本数据类型的自身对齐值。
对于char型数据,其自身对齐值为1,对于short型为2,对于int, float, double类型,其自身对齐值为4(32位系统),单位字节。
- 指定对齐值:#pragma pack (value)时的指定对齐值value。
32位机器的指定对齐值默认为4;64位机器的指定对齐值默认为8;
在32/64机器上,没有哪个基本类型的大小大于默认的指定对齐值。
即基本类型的大小都是<=默认的指定对齐值的;所以没有特别的指定对齐值时,都可不考虑指定对齐值。
- 结构体/联合体的自身对齐值:其成员中基本类型自身对齐值的最大值。
- 数据成员、结构体、联合体的有效对齐值:自身对齐值和指定对齐值中小的那个值。
有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N = 0"。
范例
#pragma pack (2) /*指定按2字节对齐*/
struct C
{
char b;
int a;
char c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1= 0;
第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续字节中,符合0x0002%2=0;
第三个变量c,自身对齐值为1,所以有效对齐值也是2,可以存放在0x0006这个字节空间中,符合0x0006%1=0。
由于整个结构体的大小需要是最大基本类型的整数倍,所以整体的结构体大小需要是:8;
gcc中的aligned属性
-
概述
GCC支持用__attribute__为变量、类型、函数、标签指定特殊属性。这些不是编程语言标准里的内容,而属于编译器对语言的扩展。
大致有六个参数值可以被设定,即:aligned,packed,transparent_union,deprecated和may_alias。
在使用__attribute__参数时,你也可以在参数的前后都加上“__”(两个下划线);
例如,使用__aligned__而不是aligned,这样,你就可以在相应的头文件里使用它而不需要关心头文件里是否有重名的宏定义。 -
aligned属性
aligned属性最常用在变量声明、类型定义上。
- attribute((aligned(n))):
此属性指定了指定类型的变量的最小对齐。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。即实际的对齐:max(aligned指定的最小对齐,默认对齐);
- aligned属性和结构体大小
当用aligned修饰结构体时:
- 结构体的大小:
最终大小 = max(最大基本类型成员大小,aligned指定的对齐大小)的整数倍;
- 结构体成员的偏移量:
偏移量=max(结构体成员中最大基本类型成员,aligned指定的偏移量);
struct __attribute__ ((aligned (8))) my_struct1 {
int i;
char a;
int f[3];
};
在上面的例子中,这个结构体内本身的字节数为:4+4+4*3=20字节;
但由于指定了8字节对齐,编译器会在该结构体尾部填充额外的4个字节,20+4是8的整数倍;
- 范例
struct __attribute__ ((aligned (8))) my_struct1 {
int i;
char a;
int f[3];
};
struct __attribute__ ((aligned(2))) my_struct2 {
int i;
char a;
int f[3];
char b;
};
struct my_struct3 {
char a;
struct my_struct1 s1;
int b;
char c;
struct my_struct2 s2;
char d;
};
结果:
sizeof(my_struct1)=24.
sizeof(my_struct2)=24.
sizeof(my_struct3)=72.
offset of b=0x20, offset of c=0x24, offset of d=0x40, offset of s2=0x28.
gcc的packed属性
attrubte ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
packed属性的主要目的是让编译器更紧凑地使用内存。
struct __attribute__ ((packed)) my_struct3 {
char c;
int i;
};
结构体被指定了packed,所以它的成员变量将是1字节对齐的,也就是说成员i将紧跟着成员c,
从而使得该结构体的实际大小为5字节。
如果不指定packed,由于要满足成员i的4字节对齐要求(它是int型的),编译器将在成员c之后填充3个字节,
使得这个结构体实际大小变为8字节。
优缺点
采用packed属性虽然可以节省内存,但它会导致非对齐的内存访问。
例如上述结构体的int型成员变量i,它的内存地址将不是4的倍数,访问它时就是非对齐访问。
当用.或->操作符存取结构体成员时,编译器会保证存取到正确的值;
但如果用指针直接访问非对齐的成员变量,就只能指望处理器支持非对齐访问了,否则将会出错。 这也是很多人认为给结构体指定packed属性不太安全的原因。
适用场景
使用packed最重要的场合莫过于处理跟文件格式或网络协议有关的二进制数据了。这些格式或协议是不能容忍多余字节的,所以当用结构体表示其数据时,必须阻止编译器填充字节。 这正是packed的设计初衷。
另外还可以用packed来节省内存,不过这是以牺牲性能为代价的,不是什么好方法。通过适当排列结构体成员的顺序可以使得需要填充的字节数尽可能少,这才是应该考虑的措施。
范例
struct unpacked_struct {
char c;
int i;
};
struct packed_struct_1 {
char c;
int i;
} __attribute__((__packed__));
struct packed_struct_2 {
char c;
int i;
struct unpacked_struct us;
} __attribute__((__packed__));
* 输出:
* sizeof(struct unpacked_struct) = 8
* sizeof(struct packed_struct_1) = 5
* sizeof(struct packed_struct_2) = 13
参考
https://www.cnblogs.com/betterquan/p/11416094.html
https://www.cnblogs.com/hellovan/p/11379743.html
https://juejin.cn/post/6844904025482100743
https://www.keil.com/support/man/docs/armclang_ref/armclang_ref_chr1359124979923.htm