一:字节对齐的概念
针对字节对齐,百度百科的解释如下:
字节对齐是字节按照一定规则在空间上排列,字节(Byte)是计算机信息技术用于计量存储容量和传输容量的一种计量单位,一个字节等于8位二进制数,在UTF-8编码中,一个英文字符等于一个字节,字节按照一定规则在空间上排列就是字节对齐。
上面就提到按照一定规则,那规则是什么,按什么规则去对齐,带着这个疑问往下走
二:为什么要字节对齐
我们为什么要进行字节对齐,不对齐会有什么后果,在计算机中我们任何一个动作无非就是保证程序的正确性,提高程序的性能和可靠性。
1,平台的硬要求,必须要字节对齐
某些平台对特定类型的数据只能从特定地址开始存取,而不允许其在内存中任意存放。例如,Motorola 68000 处理器不允许16位的字存放在奇地址,否则会触发异常,因此在这种架构下编程必须保证字节对齐。
2,提高程序的性能
字节对齐如何能提高程序的性能?CPU内部有几个重要的部件决定了CPU一次能处理的字节和可访问的内存大小。寄存器,ALU和数据总线的位数,这些共同决定了CPU的字长,常见CPU的字长有4位,8位,16位,32位和64位。字长越多,则CPU内部硬件规模和造价越高。如果CPU字长是16位。它的寄存器和总线也是16位。那么它一次处理的数据长度就为2字节。
当访问一个变量时,当该变量的地址为偶地址(即字变量的低字节在偶地址单元,高字节在奇地址单元),则需要一个总线周期访问该字变量;如果该字变量的地址为奇地址(即字变量的低字节在奇地址单元,高字节在偶地址单元),则需要用两个连续的总线周期才能访问该字变量,每个周期访问一个字节。
字节对齐让CPU读取数据的效果高了,这就解释了为什么字节对齐能提高程序的性能。
3,节省程序的内存
下面我们以一个实际的例子来看看字节对齐如何节省内存
#include <stdio.h>
#include <stdlib.h>
struct byte1
{
char a;
int b;
short c;
};
struct byte2
{
char a;
short c;
int b;
};
int main()
{
// please write your code here
printf("struct byte1 size:%d\n",sizeof(struct byte1));
printf("struct byte2 size:%d\n",sizeof(struct byte2));
}
可以看到,同样是存储一个char,一个int,一个short,结构体中顺序不一样,结构体的所占的空间也不一样。之所以出现上述结果,就是因为编译器要对数据成员在空间上进行对齐
三:字节对齐规则
1,基本类型对齐规则
基本类型包括char、int、float、double、short、long等基本数据类型。CPU位数不同所占的字节数也不一样,如下图所示
对齐要求:起始地址为其长度的整数倍即可。如,int类型的变量起始地址要求为4的整数倍,char类型的变量只占一个字节,那起始地址放哪都行。
2,结构体对齐规则
1>每个数据成员的起始位置必须是自身大小的整数倍;
2>结构体总大小必须是结构体成员中最大的对齐模数的整数倍;
3> 结构体包含数组时,按单个类型对齐方式;
4>共用体union取成员的最大内存,但包含在结构体内时,按union内部最大类型字节数的整数倍开始存储;
struct byte1
{
char a;
int b;
short c;
};
结构体大小:12
解释:char占一个字节,int占四个字节,由于int的起始地址要在4的倍数上,char后边补齐3个字节,shor占两个字节,但是整个结构体大小要是最大的对齐模数的整数倍,即4的倍数,所以补两个字节,一共12个字节
struct byte2
{
char a;
short c;
int b;
};
结构体大小:8
解释:char占一个字节,short占两个字节,由于short的起始地址要在2的倍数上,char后边补齐1个字节,int占四个字节,刚好在4的倍数上,所以总共8个字节
那结构体里嵌套结构体呢?
结构体包含另一个结构体成员,则被包含的结构体成员要从其原始结构体内部的最大对齐模数的整数倍地址开始存储(比如struct a里含有struct b,b中有char、double 、int 元素,那么b应该从8(double)的整数倍开始存储)
结构体嵌套共同体
结构体包含共用体成员,则该共用体成员要从其原始共用体内部成员中的最大对齐模数的整数倍地址开始存储
结构体最后包含0数组
struct byte2
{
char a;
short c;
int b;
double d[0];
};
结构体最后包含0数组,那0数组占空间吗?长度为0的数组的主要用途是为了满足需要可变长度的结构体,具体用法是在一个结构体的最后,申明一个长度为0的数组,就可以使得这个结构体是可变长的。对于编译器来说,此时长度为0的数组并不占用空间,因为数组名本身不占空间,它只是一个偏移量,数组名这个符号本身代表了一个不可修改的地址常量。
3,共同体对齐规则
共同体的内存除了取最大成员内存外,还要保证是所有成员类型size的最小公倍数。
union byte3
{
char a;
short c[5];
int b;
};
共同体byte3中最大成员就是short c[5],占10个字节,由于要保证是所有成员类型size的最小公倍数,即4个倍数,所以是12
4,存在#pragma pack宏的对齐规则
#pragma pack(n)//编译器将按照n个字节对齐
#pragma pack()//取消自定义字节对齐方式
******对齐规则******
结构体、联合、类的结构成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和自身对齐模数中最小的那个。
结构体的大小是#pragma pack指定的数值的整数倍。
#pragma pack(4)
typedef struct
{
int age;
char name[0];
double a;
} Person;
#pragma pack();//结束#pragma pack(4)对齐。 如果没有结束,aa也按照#pragma pack(4)对齐
typedef struct
{
double age;
Person k;
} aa;
int m=sizeof(Person); // m=12, 按照4字节对齐
int n=sizeof(aa); // n=24, 按照8字节对齐 按照#pragma pack(4)对齐的话,n=20
5,位域字节对齐规则
“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。使用位域的主要目的是压缩存储。
位域列表的形式为: 类型说明符 位域名:位域长度(单位:位 bite)
如:struct bs
{
int a:8;
int b:2;
int c:6;
} data;
其中位域a占8位,位域b占2位,位域c占6位。
位域说明:
1. 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。
2. 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节(8位)的长度,也就是说不能超过8位二进位。
3. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如: int :2
位域对齐规则
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3) 如果相邻的位域字段的类型不同,从新的存储单元开始,偏移量为其类型大小的整数倍,即不压缩;(各编译器的具体实现有差异,VC6采取不压缩方式,Dev-
C++采取压缩方式)
4) 如果位域字段之间穿插着非位域字段,则不进行压缩;
5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。
例题:
(1)typedef struct AA
{
unsigned int b1:5;
unsigned int b2:5;
unsigned int b3:5;
unsigned int b4:5;
unsigned int b5:5;
}AA;
sizeof(AA)= 4
【解析】参考规则 1)。由于相邻成员类型相同,unsigned int为 4 个字节,b1占5位,b2加上b1的位数之和为10位,不超过4字节,因此b2接着b1继续存储;
同理b3、b4、b5的类型相同,位数之和不超过4字节,因此接着b2继续存储,总位数为25位。
由于结构体的大小是最宽类型成员的整数倍,因此25位之后的补0,直到补满4字节。
(2)typedef struct AA
{
unsigned int b1:5;
unsigned int b2:5;
unsigned int b3:5;
unsigned int b4:5;
unsigned int b5:5;
unsigned int b6:5;
unsigned int b7:5;
}AA;
sizeof(AA)= 8
【解析】参考规则 1) 和规则 2) 。由于相邻成员类型相同,unsigned int为 4 个字节(32位),当存储到 b7 时,b7和b6之前的位数相加超过4字节,
因此b7从新的存储单元开始存储。
即b1~b6 存储在 第0~29位,第30、31位补0,b7从下一个 4字节存储单元 开始存储5位,剩下的补0。
(3)struct test1
{
char a:1;
char :2;
long b:3;
char c:2;
};
sizeof(test1)= 12
【解析】
char a:1; //用一个字节去存储
char :2; //空域。因为与前面的a的类型相同,而两个位域的位宽相加仍然少于8位,所以依然用1个字节表示
long b:3; //long类型的位宽是4个字节,与前面的char类型不同,所以b与a之间偏移4个字节,它们之间自动补充3个字节
char c:2; //因为c与b又不同型,以test1中的最长的long类型的位宽进行偏移,所以虽然char只用1个字节就够了,但依然要占4个字节。
结构体总长以最长的类型位宽做为偏移量,最长的是long型,占4位,所以不同类型之间应该是4个字节的偏移,即test1应该是4字节的整数倍。
总共是12字节。