C/C++ 字节对齐问题
对齐用法详解
-
什么是对齐,以及为什么要对齐: 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。 对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。
-
对齐的实现:通常,我们写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。 但是,正因为我们一般不需要关心这个问题,所以因为编辑器对数据存放做了对齐,而我们不了解的话,常常会对一些问题感到迷惑。最常见的就是struct数据结构的sizeof结果,出乎意料。为此,我们需要对对齐算法所了解。
作用:指定结构体、联合以及类成员的packing alignment;
语法:#pragma pack( [show] | [push | pop][, identifier], n )
说明:
1,pack提供数据声明级别的控制,对定义不起作用;
2,调用pack时不指定参数,n将被设成默认值;
3,一旦改变数据类型的alignment,直接效果就是占用memory的减少,但是performance会下降。
语法具体分析:
1,show:可选参数;显示当前packing aligment的字节数,以warning message的形式被显示;
2,push:可选参数;将当前指定的packing alignment数值进行压栈操作,这里的栈是the internal compiler stack,同时设置当前的packing alignment为n;如果n没有指定,则将当前的packing alignment数值压栈;
3,pop:可选参数;从internal compiler stack中删除最顶端的record;如果没有指定n,则当前栈顶record即为新的packing alignment数值;如果指定了n,则n将成为新的packing aligment数值;如果指定了identifier,则internal compiler stack中的record都将被pop直到identifier被找到,然后pop出identitier,同时设置packing alignment数值为当前栈顶的record;如果指定的identifier并不存在于internal compiler stack,则pop操作被忽略;
4,identifier:可选参数;当同push一起使用时,赋予当前被压入栈中的record一个名称;当同pop一起使用时,从internal compiler stack中pop出所有的record直到identifier被pop出,如果identifier没有被找到,则忽略pop操作;
5,n:可选参数;指定packing的数值,以字节为单位;缺省数值是8,合法的数值分别是1、2、4、8、16。
对齐方式
对于 struct 或 union 中的 struct 或者 union 来说,它们的字节对齐标准就是它的所有成员中字节数最大的数据的字节数。
一般情况下 C/C++ 的变量所占用的字节数
char: 1字节;
short: 2字节;
int: 4字节;
long: 4字节;
long long: 8字节;
float: 4字节;
double: 8字节;
bool: 1字节;
*struct 中字节对齐需要满足的条件:
1、某个变量存放的起始位置相对于结构的起始位置的偏移量是该变量字节数的整数倍;
2、结构所占用的总字节数是结构种字节数最长的变量的字节数的整数倍。
*union 中字节对齐需要满足的两个条件:
1、unoin 的大小必须足够容纳最宽的成员;
2、union 的大小需要能够被其所包含的基础成员类型的大小所整除。
VC提供了 #pragma pack(n) 用来自定义字节对齐方式
例:
1 #pragma pack(push)//保存对齐状态
2 #pragma pack(4)//设定4字节对齐
3 struct test
4 {
5 char m1;
6 double m4;
7 int m3;
8 };
9 #pragma pack(pop)//恢复对齐状态
解析:
以上结构体的大小为16,下面分析其存储情况,首先为m1分配空间,其偏移量为0,满足我们自己设定的对齐方式(4字节对齐),m1大小为1个字节。接着开始为m4分配空间,这时其偏移量为1,需要补足3个字节,这样使偏移量满足为n=4的倍数(因为sizeof(double)大于4),m4占用8个字节。接着为m3分配空间,这时其偏移量为12,满足为4的倍数,m3占用4个字节。这时已经为所有成员变量分配了空间,共分配了16个字节,满足为n的倍数。如果把上面的#pragma pack(4)改为#pragma pack(8),那么我们可以得到结构的大小为24。
实例:
1、
1 #include <stdio.h>
2 union
3 {
4 char x[5];
5 int i;
6 }a;
7
8 int main()
9 {
10 a.x[0] = 10;
11 a.x[1] = 1;
12 printf("%d\n", a.i);
13 printf("%d\n", sizeof(a));
14 return 0;
15 }
输出为 266 8
解析:
对 union 分配内存涉及字节对齐问题,在上方已有详细描述,在此只简单解释一番。union 分配的内存必须是 union 中所有基本数据类型的倍数。
在此题中即为 1 和 4 的倍数,又 char x[5] 占用 5 个字节,故 union 分配的内存大小应为 8 个字节。
windows 系统中高字节在后,低字节在前。而 char x[5] 只有前两个元素有值,即两个值只占 2 个字节,也即 union 中的 int 型数据中只有低两位上有值。
即 i 的二进制表示为:
00000000 00000000 00000001 00001010
即: 2^1 + 2^3 + 2^8 = 266
也相当于十六进制 0x010A, 即: 10 * 16^0 + 1 * 16^2 = 266
这里补充一下进制转换问题,也是我一并搜集来的:
进制互相转换
十进制 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
十六进制 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
二进制 | 0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 1001 | 1010 | 1011 | 1100 | 1101 | 1110 | 1111 |
十六进制数字转换十进制数字:
0x2AF5转换十进制:
5 * 16^0 = 5
F * 16^1 = 240
A * 16^2 = 2560
2 * 16^3 = 8192
---------------
5 + 240 + 2560 + 8192 = 10997
十进制数字转换十六进制数字:
2500转换十六进制:
2500 / 16 = 156 …… 4
156 / 16 = 9 …… 12©
9 / 16 = 0 …… 9
----------------------
得到4C9,倒转9C4,即2500的十六进制表示是: 0x9C4
二进制数字转换十六进制数字:
101110011011.1001
采取四合一法,即以二进制的小数点为分界点,向左(或向右)每四位取一位。
组分好以后,对照二进制与十六进制数的对应表,将四位二进制按权相加,得到的数就是一位十六进制数,然后按顺序排列,小数点的位置不变。
结果为:B9B.9
需要注意的是,在向左(或向右)取四位时,取到最高位(最低位)如果无法凑足四位,就可以在小数点的最左边(或最右边)补0,进行换算。
十六进制数字转换二进制数字:
对照二进制与十六进制数的对应表,将十六进制数分为二进制数字,用四位二进制数字按权相加,最后得到二进制数字,小数点依旧。
2、
1 #include <iostream>
2 #pragma pack(4)
2 using namespace std;
3
4 typedef struct A
5 {
6 char aChar;
7 int aInt_2;
8 short aInt;
9 }TypeA;
10
11 typedef struct B
12 {
13 char bChar[3];
14 TypeA bA;
15 double bDouble;
16 }TypeB;
17
18 int main()
19 {
20 TypeA a;
21 TypeB b;
22 cout << sizeof(a) << endl << sizeof(b) << endl;
23 return 0;
24 }
输出为 12 24
解析:
由上述对字节对齐问题的讨论很容易便可以得出此题的答案。
sizeof(TypeA) = 1 + 3 + 4 + 2 = 10;
sizeof(TypeB) = 3 + 1 + 12 + 8 = 24。之所以补上 1 个字节是因为 TypeA 类型在 struct TypeB 中的默认对齐方式
是 4 个字节(即 int 的字节大小,也即 struct TypeA 中最长的字节)。