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整除的地址开始,若起始地址不能被整除,则向后移动,直到能被整除,空出来的空间直接浪费,用空间换时间。
如果嵌套了结构体的情况,内层嵌套的结构体成员对齐到自己的最大对齐数的整数倍的地址,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
为什么要偏移
平台的原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取到某些特定类型的数据,否则抛出硬件异常。
性能的原因: 数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
补充
结构体字节对齐不是C++语言的“规定”,而是计算机硬件(CPU/内存控制器)的物理特性要求:CPU访问内存时并非逐字节读取,而是按“固定字长(如4字节、8字节)”批量读取,不对齐的内存会触发CPU额外的读取/拼接操作,大幅降低效率;同时部分硬件(如某些嵌入式CPU)甚至不支持非对齐访问,直接触发硬件异常。
一、字节对齐的底层原因(硬件视角)
1. CPU的内存访问机制
现代CPU的内存总线宽度是固定的(如32位CPU总线宽度4字节,64位CPU8字节),CPU访问内存时会以“总线宽度”为单位(称为内存存取粒度)一次性读取一块内存,而非逐字节读取。
举个例子(32位CPU,存取粒度4字节):
- 对齐的内存:数据起始地址是4的倍数(如0x0000、0x0004),CPU只需1次读取就能拿到完整数据;
- 未对齐的内存:数据跨两个存取块(如起始地址0x0002),CPU需要先读0x00000x0003,再读0x00040x0007,然后拼接出目标数据,耗时翻倍。
2. 硬件兼容性要求
部分硬件(如ARM架构的嵌入式CPU、老式x86处理器)不支持非对齐的内存访问,如果强行读取未对齐的数据,会直接触发“对齐错误”(硬件异常),导致程序崩溃;即使是支持非对齐访问的x86 CPU,也会通过硬件自动处理,但代价是性能大幅下降。
二、结构体字节对齐的实际影响(代码视角)
先看一个直观示例,理解对齐前后的内存占用和访问效率差异:
1. 未对齐的内存布局(假设取消对齐)
// 假设取消字节对齐,按实际大小紧凑排列
struct Unaligned {
char a; // 1字节
int b; // 4字节
short c; // 2字节
};
// 总大小:1+4+2=7字节
此时int b的起始地址是0x0001(非4的倍数),CPU读取b时需要2次内存访问,效率极低。
2. 对齐后的内存布局(默认规则)
// 默认字节对齐(按最大成员大小4字节对齐)
struct Aligned {
char a; // 1字节 → 填充3字节(凑4字节)
int b; // 4字节 → 刚好对齐
short c; // 2字节 → 填充2字节(凑4字节,整体对齐)
};
// 总大小:1+3+4+2+2=12字节(而非7字节)
char a后填充3字节,使int b的起始地址是4的倍数(对齐);short c后填充2字节,使整个结构体大小是最大成员(4字节)的整数倍(保证数组存储时每个元素都对齐);- CPU读取
b时只需1次访问,效率提升100%。
三、C++结构体字节对齐的核心规则
编译器默认遵循“自然对齐”规则,核心原则:
- 成员对齐:每个成员的起始地址必须是其自身大小的整数倍(如
char(1)任意地址,short(2)偶地址,int(4)4的倍数地址,double(8)8的倍数地址); - 整体对齐:结构体总大小必须是其“最大成员大小”的整数倍(保证结构体数组中每个元素的起始地址都满足成员对齐规则);
- 填充字节:编译器自动在成员之间/末尾插入空字节(填充),满足以上规则。
常见类型的对齐要求(x86/x64默认)
| 数据类型 | 大小(字节) | 对齐要求(起始地址) |
|---|---|---|
| char | 1 | 任意地址 |
| short | 2 | 2的倍数 |
| int/float | 4 | 4的倍数 |
| double | 8 | 8的倍数(x64)/4的倍数(x86) |
| 指针 | 4(x86)/8(x64) | 4/8的倍数 |
四、字节对齐的权衡:空间 vs 性能
- 对齐的代价:占用更多内存(如上例从7字节→12字节,内存利用率降低);
- 对齐的收益:CPU访问效率提升(核心目标),避免硬件异常;
- 灵活调整:C++支持通过
#pragma pack(n)(编译器扩展)手动设置对齐系数(n为1/2/4/8),牺牲性能换空间(如嵌入式/网络协议场景):#pragma pack(1) // 按1字节对齐(紧凑排列,无填充) struct Packed { char a; int b; short c; }; #pragma pack() // 恢复默认对齐 // 总大小:1+4+2=7字节(无填充),但访问int b时性能下降
总结
- 核心目的:适配CPU的内存存取粒度,提升访问效率,满足硬件访问规则(避免异常);
- 底层原理:CPU按固定字长批量读内存,未对齐数据需要多次读取+拼接,效率极低;
- 规则本质:编译器通过插入填充字节,保证每个成员起始地址是自身大小的整数倍,结构体总大小是最大成员的整数倍;
- 实用技巧:
- 性能优先(普通程序):使用默认对齐;
- 空间优先(嵌入式/网络协议):用
#pragma pack(1)紧凑排列; - 优化结构体布局:将大小相近的成员放在一起(如char+short+int → char+int+short,减少填充)。
本文详细解析了C++中结构体成员变量的内存对齐规则,包括单个成员对齐和整体对齐的原则,以及对齐对性能和平台移植性的影响。通过实例分析了不同排列下结构体的内存占用差异,强调了对齐在节省空间和提升效率方面的重要性。

1500

被折叠的 条评论
为什么被折叠?



