代码分区:在使用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等。
指定平台
这里的平台指的是三大件:CPU + OS + Compiler
本文中实验的平台是:Intel i7 + ubuntu16.04 + gcc5.4
有了上面的基本概念了解,就可以进行分析了。
为什么要内存对齐?
原因有两点:
CPU每次寻址都是要消费时间的,如果一次取不完数据就要取多次。比如int类型的变量a占4Byte,假设在内存中没有对齐(所谓对齐,指的是内存中数据的首地址是CPU单次获取数据大小的整数倍),且存放在0x00000003 - 0x00000006处(0x00000003不是4的整数倍)。那么每次取4字节(32位宽总线)的CPU第一次取到[0x00000000 - 0x00000003],只得到变量a的1/4数据,进而需要进行第二次取数[0x00000004 - 0x00000007],为了得到int类型的一个变量,却需要两次访问内存,并且还需要拼接处理,性能较低,这是其一。
有些CPU(ARM架构的)在内存非对齐的情况下,执行二进制代码会崩溃,因为不是所有的硬件平台都能访问任意地址上的任意数据的。倘若代码移植到其他不支持的平台上,不具有可移植性,这是其二。
若在编译时,将分配的内存进行对齐,单次访问内存就可以获取数据,并且具有平台可移植性。
那谁来把我们编写的结构体,类中的成员变量进行对齐呢?
当然是编译器(Compiler)。那对齐的规则又是如何呢?
内存对齐规则
编译器提供手动指定对齐值的关键字 #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}的整数倍。
代码实测
#include < stdio.h>
#include < iostream>
using namespace std;
class Test
{
public:
char c; // offset = 0x20000000. 区间:[0x20000000,0x2000000]
int i; //offset = min{8,4}的整数倍为0x20000004. 区间:[0x20000004,0x20000007]
short s; //offset = min{8,2}的整数倍,0x20000008. 区间:[0x20000008,0x200009]
double d; //offset = min{8,8}的整数倍,0x20000010. 区间:[0x20000010,0x200017]
//整体占24字节,并且24 为 min{8,8}的整数倍,故对齐,无需在尾部填充占位。
Test(){}
~Test(){}
void fff(){}
};
class A
{
int i;
char c1;
};
class B: public A
{
char c2;
};
class C: public B
{
char c3;
};
int main()
{
cout << sizeof(char) << endl; // 1 cout << sizeof(short) << endl; // 2 cout << sizeof(int) << endl; // 4 cout << sizeof(long) << endl; // 8 cout << sizeof(double) << endl; // 8 cout << "sizeof C: " << sizeof(C) << endl; //先继承,后对齐。相当于对一个大的类进行对齐 Test test; cout << "Test class: " << sizeof(Test) << " " << sizeof(test) << endl; return 0; } 以上注释为理论分析。 现在编译,并执行输出看看是否sizeof = 24。 这里使用的GCC中的g++进行编译。 也可以用gcc,不过要链接c++的标准库(-lstdc++),否则会链接失败。 这里实验结果与理论分析的一致。 另外:如果手动设置#pragma pack(4),后效果如何呢? #pragma pack(4) class Test { public: char c; // offset = 0x20000000. 区间:[0x20000000,0x2000000] int i; //offset = min{4,4}的整数倍为0x20000004. 区间:[0x20000004,0x20000007] short s; //offset = min{4,2}的整数倍,0x20000008. 区间:[0x20000008,0x200009] double d; //offset = min{4,8}的整数倍,0x2000000C. 区间:[0x2000000C,0x200013] //整体占20字节,并且20 为 min{4,8}的整数倍,故对齐,无需在尾部填充占位。 Test(){} ~Test(){} void fff(){} }; #pragma pack() 查看结果是否为sizeof = 20呢? 显然,是和分析的一致。 总结
这里以C++的类为例,进行内存对齐分析。关于C++的内存布局,以及含有virtual函数的类,实际上还会更复杂。简单的就如最基本的类,这和C中的struct是非常类似的。
掌握C++中类的内存对齐,有助于进一步理解C++对象模型。