Reference:
相关文章:
1. 概念
现代计算机中内存空间都是按照字节(byte)
划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐
。
如果一个变量的内存地址正好位于它长度的整数倍,它就被称做自然对齐
。比如
4
4
4 字节的 int 型,其起始地址应该位于
4
4
4 字节的边界上,即起始地址能够被
4
4
4 整除,也即对齐跟数据在内存中的位置有关。
2. 为什么要对齐
- 一些平台严格要求,比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误;
- 为了使CPU能够对变量进行快速的访问。
为什么非对齐访问内存慢?举个例子:
如果 0x02~0x05 存了一个 int ,读取这个 int 就需要先读 0x01~0x04,留下 0x02~0x04 的内容,再读 0x05~0x08,留下 0x05 的内容,两部分拼接起来才能得到那个 int 的值,这样读一个 int 就要两次内存访问,效率就低了。
比较抽象的说法,比如 字节对齐 这几个字的笔画数可以想象成每个字的字节数。我们知道第三个字是‘对’,整个的第三笔是‘了’,但是第三四五六笔放在一起就没意义了。
3. 预编译命令#pragma pack(n)
用 sizeof 运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题,有时候为了内存对齐需要补齐空字节。通常写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。
这里可以打印了一下 struct 的输出结果:
struct A
{
int val1_m; // 4 bytes
double val2_m; // 8 bytes
};
A a_stru;
std::cout << "sizeof(a_stru):" << sizeof(a_stru) << std::endl;
std::cout << "address of a_stru.val1_m: " << &a_stru.val1_m << std::endl;
std::cout << "address of a_stru.val2_m: " << &a_stru.val2_m << std::endl;
sizeof(a_stru):16
address of a_stru.val1_m: 0x7ffe787bd690
address of a_stru.val2_m: 0x7ffe787bd698
可以看到 a_stru.val1_m
明明是 int 类型,只占用
4
4
4 个字节,但这里和下一个变量 a_stru.val2_m
之间的字节差为
8
8
8,这里就是编译器自动对齐了。
当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。这里引入与编译器指令#pragma pack:
-
语法详见官网:
#pragma pack( [show] | [push | pop] [, identifier], n )
-
作用:指定 structure、union 和 class 的包对齐方式(pack alignment)
#pragma pack(n) //指定对齐的字节数为 n,注意,数需要为2的幂
#pragma pack() with no parameters is deprecated. Use #pragma pack(4) instead.
#pragma pack(4) // 4-byte alignment
#pragma pack(1) // 1-byte alignment
3.1 运算符 alignof
怎么确定成员的实际大小:
- 对于基本类型,对齐值即为 sizeof() 返回值;
- 对于其他类型可以使用alignof()来查询。
- 语法详见官网:
alignof 操作符返回指定类型的以字节为单位的对齐方式,返回类型为 size_t。alignof( type )
4. 字节对齐规则
结构体中各个成员按照它们被声明的顺序在内存中顺序存储。对齐规则如下:
-
各个数据成员按各自数据类型对齐:
对齐模数是该数据成员所占内存与#pragma pack指定的数值中的较小者。 -
整个结构体字节大小向结构体模数对齐:
结构体模数
是 #pragma pack指定的数值(32位机默认4字节、64位机默认16字节) 和 结构体内部最大的基本数据类型成员 长度中数值较小者。结构体的长度应该是该模数的整数倍。
根据规则2,再写一个对应示例:
struct B
{
int val3_m; // 4 bytes
A val4_m; // 12 bytes
double val5_m; // 8 bytes
int val6_m; // 4 bytes
};
B b_stru;
std::cout << "sizeof(b_stru):" << sizeof(b_stru) << std::endl;
std::cout << "address of b_stru.val3_m: " << &b_stru.val3_m << std::endl;
std::cout << "address of b_stru.val4_m: " << &b_stru.val4_m << std::endl;
std::cout << "address of b_stru.val5_m: " << &b_stru.val5_m << std::endl;
std::cout << "address of b_stru.val6_m: " << &b_stru.val6_m << std::endl;
sizeof(b_stru):40
address of b_stru.val3_m: 0x7ffc58ed5a10
address of b_stru.val4_m: 0x7ffc58ed5a18
address of b_stru.val5_m: 0x7ffc58ed5a28
address of b_stru.val6_m: 0x7ffc58ed5a30
这里神奇的地方比较多,一行行看:
- 初始地址从 0x7ffc58ed5a10 开始,int 大小为 4 字节,但是下一个跳到了 0x7ffc58ed5a18,中间差 8 个字节。这是因为 A 是一个结构体,#pragma pack 默认 16 字节,而结构体内最大基本数据占 8 个字节,所以该结构体模数为 8,该结构体以 8 字节对齐。
- 0x7ffc58ed5a18 到 0x7ffc58ed5a28 中间 16 个字节,前文示例已说明。
- 0x7ffc58ed5a28 到 0x7ffc58ed5a30 中间 8 个字节,很正常,double 类型,没什么好说的。
- 这样算下来也就占用了 4+4+16+8+4=36 个字节,为啥最终得数是40?因为前文所说,整个结构体字节大小向 结构体模数 和 结构体内部最大的基本数据类型成员长度中数值较小者 对齐,所以结构体大小需要是 8 的整数倍,末尾有 4 个字节的空字节,为 4+4+16+8+4+4=40 字节。
这个时候再引入 #pragma pack
看看,
#pragma pack(4)
struct A
{
int val1_m; // 4 bytes
double val2_m; // 8 bytes
};
#pragma pack(1)
struct B
{
int val3_m; // 4 bytes
A val4_m; // 12 bytes
double val5_m; // 8 bytes
int val6_m; // 4 bytes
};
A a_stru;
std::cout << "sizeof(a_stru):" << sizeof(a_stru) << std::endl;
std::cout << "address of a_stru.val1_m: " << &a_stru.val1_m << std::endl;
std::cout << "address of a_stru.val2_m: " << &a_stru.val2_m << std::endl;
B b_stru;
std::cout << "sizeof(b_stru):" << sizeof(b_stru) << std::endl;
std::cout << "address of b_stru.val3_m: " << &b_stru.val3_m << std::endl;
std::cout << "address of b_stru.val4_m: " << &b_stru.val4_m << std::endl;
std::cout << "address of b_stru.val5_m: " << &b_stru.val5_m << std::endl;
std::cout << "address of b_stru.val6_m: " << &b_stru.val6_m << std::endl;
输出:
sizeof(a_stru):12
address of a_stru.val1_m: 0x7ffedaaf13d4
address of a_stru.val2_m: 0x7ffedaaf13d8
sizeof(b_stru):28
address of b_stru.val3_m: 0x7ffedaaf13e0
address of b_stru.val4_m: 0x7ffedaaf13e4
address of b_stru.val5_m: 0x7ffedaaf13f0
address of b_stru.val6_m: 0x7ffedaaf13f8
可以看到这里就没有使用空字节补齐了。