关于结构体内存对齐
内存对齐”应该是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。但是C语言的一个特点就是太灵活,太强大,它允许你干预“内存对齐”。如果你想了解更加底层的秘密,“内存对齐”对你就不应该再透明了。
一、内存对齐的原因
大部分的参考资料都是如是说的:
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
二、对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。 对齐步骤:
1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
3、结合1、2颗推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
备注:数组成员按长度按数组类型长度计算,如char t[9],在第1步中数据自身长度按1算,累加结构体时长度为9;第2步中,找最大数据长度时,如果结构体T有复杂类型成员A的,该A成员的长度为该复杂类型成员A的最大成员长度。
一。结构体大小计算基础
基础一
先介绍一个相关的概念——偏移量。偏移量指的是结构体变量中成员的地址和结构体变量地址的差。结构体大小等于最后一个成员的偏移量加上最后一个成员的大小。由于存储变量时地址对齐的要求,编译器在编译程序时会遵循两条原则:
一、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)
二、结构体大小必须是所有成员大小的整数倍。
这是一种说法,还有一种说法:
结构体默认的字节对齐一般满足三个准则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,
如有需要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末
一个成员之后加上填充字节。
我自己写了三个结构体测试了一下,代码如下:
输出结果是:
12
8
20
先看第一个结构体,第一个成员a的偏移量为0。第二个成员b的偏移量是第一个成员的偏移量加上第一个成员的大小(0+4),其值为4;第三个成员c的偏移量是第二个成员的偏移量加上第二个成员的大小(4+1),其值为5。上面的例子中前两个成员的偏移量都满足要求,但第三个成员的偏移量为5,并不是自身(int)大小的整数倍。编译器在处理时会在第二个成员后面补上3个空字节,使得第三个成员的偏移量变成8。结构体大小等于最后一个成员的偏移量加上最后一个成员的大小。偏移量8加上最后一个成员c自身的大小,即为9,9不能被最宽基本类型成员大小整除,所以这里编译器会在最后补齐3个字节,得出12。
至于第二个结构体,大小等于最后一个成员的偏移量加上其大小,上面的例子中计算出来的大小为8,满足要求。
按照上述的计算规则,结构体一和结构体二都可以计算出与输出一致的结果,可是结构体三有点不明白。//在32位机器中,double型要拆分成两个int型,如下:
偏移量 填充字节 成员自身大小
0 4
4 1
5 1 2
8 4
12 4
16 4
20
最终结果是20,在64位机器中double就按8字节算
基础二
结构体中的成员可以是不同的数据类型,成员按照定义时的顺序依次存储在连续的内存空间。和数组不一样的是,结构体的大小不是所有成员大小简单的相加,需要考虑到系统在存储结构体变量时的地址对齐问题。看下面这样的一个结构体:
struct stu1
{
int i;
char c;
int j;
};
先介绍一个相关的概念——偏移量。偏移量指的是结构体变量中成员的地址和结构体变量地址的差。结构体大小等于最后一个成员的偏移量加上最后一个成员的大小。显然,结构体变量中第一个成员的地址就是结构体变量的首地址。因此,第一个成员i的偏移量为0。第二个成员c的偏移量是第一个成员的偏移量加上第一个成员的大小(0+4),其值为4;第三个成员j的偏移量是第二个成员的偏移量加上第二个成员的大小(4+1),其值为5。
实际上,由于存储变量时地址对齐的要求,编译器在编译程序时会遵循两条原则:一、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍) 二、结构体大小必须是所有成员大小的整数倍。
对照第一条,上面的例子中前两个成员的偏移量都满足要求,但第三个成员的偏移量为5,并不是自身(int)大小的整数倍。编译器在处理时会在第二个成员后面补上3个空字节,使得第三个成员的偏移量变成8。
对照第二条,结构体大小等于最后一个成员的偏移量加上其大小,上面的例子中计算出来的大小为12,满足要求。
再看一个满足第一条,不满足第二条的情况
struct stu2
{
int k;
short t;
};
成员k的偏移量为0;成员t的偏移量为4,都不需要调整。但计算出来的大小为6,显然不是成员k大小的整数倍。因此,编译器会在成员t后面补上2个字节,使得结构体的大小变成8从而满足第二个要求。由此可见,大家在定义结构体类型时需要考虑到字节对齐的情况,不同的顺序会影响到结构体的大小。对比下面两种定义顺序
struct stu3
{
char c1;//偏移量为0符合要求
int i;//偏移量为1, 结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍),故偏移量应为4
char c2;//偏移量为8(偏移量4+int大2小4),符合要求
}
算出sizeof( stu3 )=1+8=9,但9不是int的整数倍故最终大小为12
struct stu4
{
char c1; //偏移量为0符合要求
char c2;// //偏移量为1符合要求
int i; //偏移量为2不符合要求故偏移量为4
}
算出sizeof( stu4 )=4+4=8,8是int的整数倍故最终大小为8
如果结构体中的成员又是另外一种结构体类型时应该怎么计算呢?只需把其展开即可。但有一点需要注意,展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍。看下面的例子:
struct stu5
{
short i;
struct
{
char c;
int j;
} ss;
int k;
}
结构体stu5的成员ss.c的偏移量应该是4,而不是 2。整个结构体大小应该是16。
二。结构体大小计算深入(结构体嵌套,数组,共用体)
对于32位机器,double会拆分成两个int进行计算。
对于结构体
首先理解字节对齐机制:
(1),结构体变量的首地址能够被其最宽基本类型成员大小所整除。
(2),结构体每个成员相对于结构体首地址的偏移量(offset)都是该成员大小的整数倍,如有需要,编译器会在成员之间加上中间填充字节。
(3)结构体总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上末尾填充字节。
第(2)、(3)条准则决定了结构体变量占据内存空间的大小。
基本类型指:char,short,int,float,double。
数据宽度即其sizeof的大小。
eg:
struct {char c;short s;float f;}A,
struct{char c;float f;short s;}B;
则sizeof(A)=?,sizeof(B)=?
解:A中,
char占1字节,short占2,则short相对于结构体首地址的offset(偏移量)=1,为满足准则(2),必须添加中间填充字节1;
float占4字节,相对于结构体首地址offset=(1+1+2)即4,满足准则(2)。
此时结构体大小为(1+1+2+4)=8,是最宽基本类型float整数倍,满足(3)。所以可得出:sizeof(A)=8;
对于B:B中,
char占1,float占4,则float相对于结构体首地址的offset=1,为满足准则(2),必须添加中间填充字节3;
short占2,相对于结构体首地址offset=(1+3+4)即8,是short所占字节的整数倍,满足准则(2)。
此时结构体大小为(1+3+4+2)=10,不是最宽基本类型float的整数倍,这时,末尾必须添加末尾填充字节2,所以最后总大小为12;
总结:分析时只要这样即可:
对于A:1 1 2 4 (红色为插入部分,写时,中间要留够空格)
对于B:1 3 4 2 2
这样就可以直观的得到结构体大小。
迁移eg:struct{float f;int I;short s;}C; 则sizeof(C)=?(答案:12)
知识扩展1:若出现结构体嵌套,则准则(2)、(3)应改为:
(2),复合成员相对于结构体首地址的偏移量是复合成员中最大基本类型成员大小的整数倍。
(3),结构体总大小为结构体最宽基本类型成员大小整数倍。
eg:
struct
{
float f;
char c;
double d;
struct
{
double d1;
float f1;
double d2;
char c1;
}A
}B;
则sizeof(B)=?
解:
对于64位机器:
先计算A大小:8 4 4 8 1 7(红色为插入部分),得到为32;
所以对于B,一开始排列为: 4 1 8 32;
1的offset=4,所以1与8间要添上3。变为:
4 1 3 8 32;
此时8的offset=8,满足准则2,32的offset=16,是A中最宽基本类型double所占字节的整数倍,满足准则2;此时B大小为48,是B中最宽基本类型double的整数倍,满足准则3.所以最后sizeof(B)=48;
知识扩展2,若结构体内部出现数组,则准则2应为:
(2),数组相对于结构体首地址的offset是该数组成员大小的整数倍。
eg :
struct {double d[2];char*c;}C; 则sizeof(C)=?
解:对于64位机器:
double大小为8字节,所以double[2]为16;而指针大小均为4,
所以C可排列为:
16 4 4
得到sizeof(C)=24;
对于32位机器:
double大小为8字节(拆分为连个int型),double[2]全部展开;而指针大小均为4,
所以C可排列为:
4 4 4 4 4
得到sizeof(C)=20;
对于共用体:(32位机器下)
原则上,共用体大小取决于占据最多内存的成员的所占据的数据段的大小(最大的那个数据仍然遵守字节对齐原则)。
Eg:
union uOne4All
{
char nVal[10];
long dVal;
}f;
则sizeof(f)=12。//char nVal[10]占10个字节空间,32为机器默认四字节对齐,所以占12字节空间
Union
{
char c;
int d;
}e;
则sizeof(e)=4;
Union { char c; double d; }e; 则sizeof(e)=8;//貌似虽然double拆分成两个int型计算但任然是一个整体。