1 明确几个概念
代码分区:在使用C/C++编程时,我们定义的变量存在于内存中,而内存在C语言的角度上可以分为五大区。局部变量在栈区,静态/全局变量在全局区,动态申请的变量存在于堆区,const修饰的变量/字符常量存在于只读区。无论是什么样的变量,终究在内存中。
CPU取指,译码,执行:存在于内存中的目的是为了CPU通过总线的进行寻址,取指令,译码,执行取数据,内存与寄存器交互,然后CPU运算,再输出数据至内存。这个过程反复的,高速的执行。
CPU位数:在计算机中,最小的存储单元为字节(Byte),理论上任意地址(比如0x20000002,0x20000003,0x2000011...)都可以通过总线进行访问(CPU寻址),而每次寻址后,传输的数据大小跟CPU位数相关,常见的CPU位数有8位,16位,32位,64位。位数越高,单次操作执行的数据量越大,性能也就越强。
OS位数:操作系统一般与CPU位数相匹配,32位CPU可以寻址4GB内存空间,可以运行32位的OS。同样,64位的CPU可以运行32位的OS,也可以运行64位的OS。
Compiler:虽然编译器都是在翻译/编译代码,进行预处理(宏展开,头文件展开),编译(语法检查等),汇编(翻译为机器码),链接(重定位等)这四部分的工作。但是不同的编译器的内部默认设置以及用法会有所差异,常用的有GCC,VS,Clang,MinGW等。
2 指定平台
不谈平台,只谈编码,就是在耍流氓。---哈哈哈
这里的平台指的是三大件:CPU + OS + Compiler
本文中实验的平台是:Intel i7 + ubuntu16.04 + gcc5.4
有了上面的基本概念了解,就可以进行分析了。
3 为什么要内存对齐?
原因有两点:
CPU每次寻址都是要消费时间的,如果一次取不完数据就要取多次。比如int类型的变量a占4Byte,假设在内存中没有对齐(所谓对齐,指的是内存中数据的首地址是CPU单次获取数据大小的整数倍),且存放在0x00000003 - 0x00000006处(0x00000003不是4的整数倍)。那么每次取4字节(32位宽总线)的CPU第一次取到[0x00000000 - 0x00000003],只得到变量a的1/4数据,进而需要进行第二次取数[0x00000004 - 0x00000007],为了得到int类型的一个变量,却需要两次访问内存,并且还需要拼接处理,性能较低,这是其一。
有些CPU(ARM架构的)在内存非对齐的情况下,执行二进制代码会崩溃,因为不是所有的硬件平台都能访问任意地址上的任意数据的。倘若代码移植到其他不支持的平台上,不具有可移植性,这是其二。
若在编译时,将分配的内存进行对齐,单次访问内存就可以获取数据,并且具有平台可移植性。
那谁来把我们编写的结构体,类中的成员变量进行对齐呢?
当然是编译器(Compiler)。那对齐的规则又是如何呢?
4 内存对齐规则
编译器提供手动指定对齐值的关键字 #pragma pack(N),可以手动设置对齐的字节数,比如#pragma pack(1),#pragma pack(4)等。这里即为N。
若没有手动指定,那么编译器就会默认将成员变量中最大的类型字节数设置为对齐值:m
1 整体对齐值:
首先计算对齐单位 n = min{N,m},然后整体对齐后的字节数应该为n的倍数,不够的在最后面填补占位。
2 成员对齐值:
首个成员的偏置地址(offset) = 0。
假定该成员的类型占字节数 j,那么本成员的偏移地址(offset):min{n, j}的整数倍。
5 代码实测
#include
以上注释为理论分析。
现在编译,并执行输出看看是否sizeof = 24。
这里使用的GCC中的g++进行编译。
也可以用gcc,不过要链接c++的标准库(-lstdc++),否则会链接失败。
这里实验结果与理论分析的一致。
另外:如果手动设置#pragma pack(4),后效果如何呢?
#pragma pack(4)
查看结果是否为sizeof = 20呢?
显然,是和分析的一致。
6 总结
这里以C++的类为例,进行内存对齐分析。关于C++的内存布局,以及含有virtual函数的类,实际上还会更复杂。简单的就如最基本的类,这和C中的struct是非常类似的。
掌握C++中类的内存对齐,有助于进一步理解C++对象模型。