文章目录
- 一、基本概念
- 二、对齐规则
- 三、编译器控制对齐
在C++中,类的内存对齐方式遵循以下一些原则和特点:
一、基本概念
内存对齐(Memory Alignment)也叫字节对齐,是指数据在内存中的存放地址要满足一定的规则,通常是按照数据类型自身的对齐模数来进行对齐放置。这样做的主要目的是为了提高处理器访问内存的效率,因为处理器在读取内存时往往按照字长(比如32位系统按4字节,64位系统按8字节)等单位进行读取,合适的对齐能让数据一次性被完整读取,减少读取次数。
二、对齐规则
- 数据成员对齐
对于类中的各个数据成员,编译器会按照它们各自类型对应的对齐要求来安排内存位置。常见基本数据类型的对齐要求如下:char
类型:通常按1字节对齐,也就是说char
类型的数据可以存放在任意内存地址上,因为任何地址对于1字节的访问都是对齐的。short
类型:一般按2字节对齐,意味着short
类型的数据的存储地址应该是2的倍数(例如在内存地址0、2、4等位置)。int
类型:多数情况下按4字节对齐,其存储地址需是4的倍数(像内存地址0、4、8等)。double
类型:常按8字节对齐,存储地址要是8的倍数(例如内存地址0、8、16等)。
例如,有这样一个类:
class AlignmentExample {
char c;
int i;
short s;
};
按照对齐规则,首先char
类型的c
可以存放在任意地址(假设起始地址为0),然后int
类型的i
由于要按4字节对齐,所以它会从地址4开始存放(因为前面的c
占了1字节,填充3字节使其对齐到4字节边界),最后short
类型的s
按2字节对齐,它会接着存放在地址8处(因为地址4 - 7被i
占用了,地址8刚好满足2字节对齐要求)。整个类对象的大小可能会是10字节,但考虑到内存对齐,编译器通常会填充到12字节,使得下一个对象(如果有连续存放的情况)也能按照合适的对齐要求来放置。
- 继承情况下的对齐
在继承关系中,派生类对象的内存布局会先包含基类的数据成员,并且遵循基类原有的对齐方式以及整个继承体系下的综合对齐要求。
比如:
class Base {
int baseNum;
};
class Derived : public Base {
short derivedNum;
};
Derived
类对象的内存中,先是Base
类的baseNum
按4字节对齐存放,然后Derived
类的short
类型的derivedNum
按2字节对齐存放在合适位置(在baseNum
之后,且满足2字节对齐要求)。
- 结构体(类)整体对齐
一个类或者结构体整体的大小也需要满足其内部最严格的对齐要求。也就是说,类或结构体的总大小应该是其内部最大对齐模数(通常是最大数据类型对应的对齐字节数)的整数倍。例如,如果一个类中有int
(按4字节对齐)和double
(按8字节对齐)类型的数据成员,那么这个类整体的大小要为8的倍数,编译器会在必要时填充一些字节来达到这个要求。
三、编译器控制对齐
- 指定对齐方式(以
#pragma pack
为例)
可以通过编译器指令来改变默认的对齐规则,比如在一些编译器中使用#pragma pack
指令。
例如:
#pragma pack(1)
class PackedClass {
char c;
int i;
short s;
};
#pragma pack()
#pragma pack(1)
表示设置按照1字节对齐,此时PackedClass
类对象就不会有额外的填充字节来满足其他对齐要求了,它的大小就是char
(1字节) + int
(4字节) + short
(2字节) = 7字节。而#pragma pack()
用于恢复编译器的默认对齐设置。
不同的编译器可能还支持其他方式来指定对齐参数,比如有些编译器可以使用特定的编译选项来全局设置对齐规则等。
- 属性指定(部分编译器支持)
有些编译器允许通过属性(Attributes)来指定类或者数据成员的对齐方式,例如在某些支持特定扩展的C++编译器中,可以像下面这样做:
class __attribute__((aligned(8))) AlignedClass {
// 类中的数据成员
};
上述代码通过__attribute__((aligned(8)))
属性指定AlignedClass
类按照8字节对齐,类中的数据成员以及整体的大小都会遵循这个8字节对齐的要求来布局和确定。
总之,C++类的对齐方式是综合考虑数据成员类型、继承关系以及编译器相关设置等多方面因素的,合适的对齐有助于提升程序运行时内存访问的性能。
1、结构体大小由成员变量和偏移量相加而成;
2、其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,整体大小也要能被对齐数整除
总对齐数: = 编译器默认的一个对齐数与该成员大小的较小值, 也就是min{编译器默认的对齐数, 成员变量大小的最大值},VS中默认的对齐数为8;
单个对齐数:该类型变量所占内存大小,char 对齐数是1 ,int 对齐数是4 , double 是8 , 32位指针类型是4 , 64位指针类型是8;
对齐原则分两步:
1、单个成员变量对齐(也就是说,单个变量的起始地址能被单个对齐数整除)
2、整体对齐(整体大小能被对齐数整除,若不能不整除,则增加偏移来填充)
相同的成员变量,因为声明位置不同,导致占用内存空间不同,看下面代码
void test6()
{
//总对齐数min{编译器默认的对齐数=8 ,成员变量大小的最大值double=8 } = 8
struct node
{
//假设总体空间为all
double m;//8 double对齐数为8 ,此时all=8 [0:7]
int a;//4 int 对齐数为 4 all=12 [8:11]
char c;//1 char对齐数 1 all=13 [12]
char b;//1 char对齐数 1 all=14 [13]
};//16 开始整体对齐,要能被整体对齐数整除,也就是能被8整除,14不能被8整除,+2=16 即可,所以all=16
struct node1
{
char c; //1 char对齐数 1 all=1
int a;//4 int对齐数 4 (char c 后空出三个字节,从第四个字节开始存放 int a) all=8
char b; //1 char对齐数 1 all=9
double m;//8 double对齐数为8 (char b 后空出7个字节,从第四个字节开始存放double m) ,此时all=24
};//24 24能被8整除,所以整体不需要偏移
cout << sizeof(node) << "---" << sizeof(node1) << endl;
}
单个成员变量对齐(也就是说,单个变量的起始地址能被单个对齐数整除)更加通俗的讲就是;double类型必须从被8整除的地址开始,int 必须从被4整除的地址开始,若起始地址不能被整除,则向后移动,直到能被整除,空出来的空间直接浪费,用空间换时间。
如果嵌套了结构体的情况,内层嵌套的结构体成员对齐到自己的最大对齐数的整数倍的地址,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
为什么要偏移
平台的原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取到某些特定类型的数据,否则抛出硬件异常。
性能的原因: 数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。