内存对齐
一、什么是内存对齐?
1.概念
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是在实际的计算机系统中,对基本类型数据在内存中存放的位置有限制,系统要求这些数据的首地址的值是某个数k的倍数,这就是所谓的内存对齐;如上,由于int型在32位的系统中占4个字节,而char型占1个字节;那么这个结构体s是占4+1 = 5个字节么?自然不是,由于内存对齐的机制存在,得到的结果是8。这个结果是怎么得到的,我们下面来解答;
2.为什么要进行内存对齐?
尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存,它一般会以2字节、4字节、8字节、16字节、甚至是32字节为单位来存取内存,我们将上述这些存取单位成为内存存取粒度;现在考虑4字节存取粒度的处理器取int类型变量(32位系统),该处理器只能从地址为4的倍数的内存开始读取数据。假如没有内存对齐机制,数据可以任意存放,现在一个int型变量存放在从地址1开始的连续4个字节的地址中,那么该处理器取数据时,要先从0地址开始读取第一个4字节块,剔除不需要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5、6、7地址),最后留下的两块数据合并放入寄存器中,这里做了许多不必要的工作。
现在有了内存对齐,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在处理器在读取这个数据的时候,就可以一次性将数据读出来了,而不需要其他的操作,这提高了效率;
二、内存对齐规则
1.对齐系数
每个平台上的编译器都有自己的默认“对齐系数”(也叫对齐模式)。gcc中默认#pragma pack(4),也就是按照4字节对齐,我们可以通过预编译命令#pragma pack(n) , n = 1、2、4、8、16来改变这一系数;2.有效对齐值
是给定值#pragma pack(n)和结构体中最长数据类型长度中较小的那个,也就是min(n , max(struct))。有效对齐值也称之为对齐单位;3.规则
(1)结构体中第一个成员变量的偏移量(offset)为0 , 以后每个成员变量相对于结构体首地址的offset都是该成员大小与有效对齐值中较小的那个数的整数倍,如有需要编译器会在成员之间加上填充字节;(2)结构体的总大小为有效对齐值的整数倍,如有需要编译器会自动在最末一个成员之后加上相应的填充字节;
4.示例
(1)在linux环境下,默认的#pragma pack(4),且结构体中最长的数据类型为4个字节,所以有效对齐单位为4个字节;
分析:
(2)如果在前面加上#pragma pack(1)的话,此时的有效对齐值为min(4 , 1) = 1, 所以根据这个对齐规则的话,各个结构体的字节数为:
(3)如果加上#pragma pack(2),有效对齐值为2字节,那么根据这个对齐规则各个结构体的字节数为:
三、#pragma pack()语法问题
1.基本数据类型的所占内存大小
2.使用#pragma pack(n)可以指定对齐系数,用#pragma pack()可以取消自定义对齐方式,恢复默认对齐。
3.默认对齐系数:
32位gcc---》4;64位gcc---》8;
一般32位为4字节,64位为8字节;
4.#pragma pack(push)、#pragma pack(pop)
如果有些时候,一会想要4字节,有时候又想1字节或者8字节对齐,那么该如何解决呢?这个时候就要用到#pragma pack(push)、#pragma pack(pop)了。(1)#pragma pack(push)
编译器编译到此时的时候将保存push指令之前的对齐状态;
(2)#pragma pack(pop)
编译器编译到此时的时候将恢复push指令之前保存的对齐状态
因此,push和pop是一对应该同时出现的指令,示例如下:
请注意:#pragma pack(pop)是恢复到push之前的对齐方式,而#pragma pack()是恢复默认对齐方式;
5.#pragma pack(push , n)和#pragma pack(pop , n)
等同于#pragma pack(push)#pragma pack(n)
等同于#pragma pack(pop)
#pragma pack(n)//这里不再恢复到push之前的对齐方式了,而是使用新的对齐方式n
6. #pragma pack(show)
显示当前内存对齐的字节数,使用这条编译指令在编译阶段会显示一个警告,说明当前的对齐字节数7._attribute_(aligned(n))和_attribute_((packed))
四、其他情况
1、静态变量static
静态变量的存放位置与结构体实例的存储地址无关,是单独存放在静态数据区的,因此使用sizeof计算结构体大小的时候,不要将静态成员所占的空间计算进来;2、类
空类是会占用内存空间的,大小为1,这是因为c++要求每个实例在内存中都有独一无二的地址;(1)类内部的成员变量
【1】普通变量:是要计算内存的
【2】static修饰的静态变量:不占用内存,编译器将其放在了全局变量区
(2)类内部的成员函数
【1】普通函数:不占用内存
【2】虚函数:占4个字节,用来指定虚函数表的入口地址。所以一个类的虚函数占用的空间大小是不变的,和虚函数的个数是没有关系的;
(3)子类
子类所占内存大小是父类+自身成员变量的值;特别注意的是,子类与父类共享同一个虚函数指针,因此当子类新声明一个虚函数的时候,不需要对齐保存虚函数表的入口