结构体大小

本文详细解析了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整除的地址开始,若起始地址不能被整除,则向后移动,直到能被整除,空出来的空间直接浪费,用空间换时间。

如果嵌套了结构体的情况,内层嵌套的结构体成员对齐到自己的最大对齐数的整数倍的地址,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

为什么要偏移
平台的原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取到某些特定类型的数据,否则抛出硬件异常。
性能的原因: 数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

补充

结构体字节对齐不是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,然后拼接出目标数据,耗时翻倍。

32位CPU内存存取粒度:4字节

对齐数据:地址0x0000

1次读取:0x0000-0x0003,直接获取完整数据

未对齐数据:地址0x0002

第1次读取:0x0000-0x0003(取后2字节)

第2次读取:0x0004-0x0007(取前2字节)

CPU拼接数据,耗时增加

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++结构体字节对齐的核心规则

编译器默认遵循“自然对齐”规则,核心原则:

  1. 成员对齐:每个成员的起始地址必须是其自身大小的整数倍(如char(1)任意地址,short(2)偶地址,int(4)4的倍数地址,double(8)8的倍数地址);
  2. 整体对齐:结构体总大小必须是其“最大成员大小”的整数倍(保证结构体数组中每个元素的起始地址都满足成员对齐规则);
  3. 填充字节:编译器自动在成员之间/末尾插入空字节(填充),满足以上规则。
常见类型的对齐要求(x86/x64默认)
数据类型大小(字节)对齐要求(起始地址)
char1任意地址
short22的倍数
int/float44的倍数
double88的倍数(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时性能下降
    

总结

  1. 核心目的:适配CPU的内存存取粒度,提升访问效率,满足硬件访问规则(避免异常);
  2. 底层原理:CPU按固定字长批量读内存,未对齐数据需要多次读取+拼接,效率极低;
  3. 规则本质:编译器通过插入填充字节,保证每个成员起始地址是自身大小的整数倍,结构体总大小是最大成员的整数倍;
  4. 实用技巧
    • 性能优先(普通程序):使用默认对齐;
    • 空间优先(嵌入式/网络协议):用#pragma pack(1)紧凑排列;
    • 优化结构体布局:将大小相近的成员放在一起(如char+short+int → char+int+short,减少填充)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值