图文并茂理解结构体大小-内存对齐

一、基础:成员类型大小——结构体大小的“计算起点”

结构体的本质是“多个不同类型数据的集合”,每个成员的自身大小是计算结构体总大小的“原始素材”,由编程语言和硬件架构共同定义(以主流的32/64位架构为例):

数据类型

32位架构(如ARM32)

64位架构(如ARM64/x86_64)

说明

char

1字节(8位)

1字节(8位)

最小整数类型,无符号/有符号

short

2字节(16位)

2字节(16位)

短整数

int

4字节(32位)

4字节(32位)

标准整数(兼容32位操作)

long

4字节(32位)

8字节(64位)

架构差异核心点

long long

8字节(64位)

8字节(64位)

64位整数

float

4字节(32位)

4字节(32位)

单精度浮点数

double

8字节(64位)

8字节(64位)

双精度浮点数

指针(void*

4字节(32位地址)

8字节(64位地址)

指向内存地址的变量

关键结论

每个成员的大小是固定的(如char恒为1字节),但结构体总大小不是成员大小的简单相加——因为内存对齐规则会插入“填充字节”,这是理解结构体大小的核心。

二、核心:内存对齐规则——为什么需要对齐?(硬件层面的底层逻辑)

内存对齐的本质是硬件访问效率的妥协:

计算机的CPU访问内存时,并非“按字节逐个读取”,而是按“固定块大小”(如ARM32的4字节、ARM64的8字节)批量读取,如下图,每一次读取都是读取四个字节。

若数据存放在“非对齐地址”(如4字节的int存放在地址1-4),CPU需要:

  1. 先读取地址0-3的块,提取其中的1-3字节;

  2. 再读取地址4-7的块,提取其中的4字节;

  3. 最后拼接这两部分数据——额外增加1次内存访问,效率降低

因此,内存对齐规则的目标是:让每个成员都存放在“CPU可高效读取的地址”上,避免额外开销。

三、细节:内存对齐的3条核心规则(决定填充字节的位置和数量)

结构体的对齐规则可拆解为3条,需严格按顺序执行,最终决定总大小:

规则1:成员的“自身对齐要求”——每个成员有固定的对齐地址

每个数据类型都有“自身对齐值”(Alignment Requirement),成员必须存放在“自身对齐值的整数倍地址”上。

常见类型的默认自身对齐值(与大小一致,编译器可调整):

  • char:自身对齐值=1(任意地址均可,如0、1、2...);

  • short:自身对齐值=2(地址必须是2的倍数,如0、2、4...);

  • int/float:自身对齐值=4(地址必须是4的倍数,如0、4、8...);

  • long(ARM64)/double/指针:自身对齐值=8(地址必须是8的倍数,如0、8、16...)。

int成员的地址必须是4的倍数(如0、4、8),若下一个可用地址是1,则需填充3字节(地址1-3),将int放在地址4。

规则2:成员间的“填充字节”——满足前一个成员的对齐,为下一个成员腾空间

当一个成员的末尾地址不满足下一个成员的“自身对齐值”时,需在两个成员之间插入“填充字节”,直到下一个地址满足对齐要求。

:结构体struct { char a; int b; }(ARM32):

  • char a占地址0(1字节),末尾地址是0;

  • 下一个成员int b的自身对齐值=4,需地址是4的倍数;

  • 当前可用地址是1(不满足4的倍数),需填充3字节(地址1-3),将b放在地址4;

  • 成员间填充3字节,此时已占用地址0-7(共8字节)。

规则3:结构体的“整体对齐要求”——总大小需是“最大自身对齐值”的整数倍

结构体作为一个整体,其总大小必须是“所有成员中最大自身对齐值”的整数倍——若当前总大小不满足,需在结构体末尾插入“末尾填充字节”。

原因:当结构体作为数组元素时(如struct Test arr[2]),第二个元素的起始地址需满足结构体的整体对齐要求,否则数组中的成员会出现非对齐访问。

:结构体struct { char a; int b; char c; }(ARM32):

  • 成员a(0-0)、b(4-7,中间填充3字节)、c(8-8);

  • 已占用地址0-8(共9字节);

  • 所有成员的最大自身对齐值=4(int的对齐值);

  • 9不是4的倍数,需在末尾填充3字节(地址9-11),总大小=12字节(4的3倍)。

四、实战:结合案例,完整计算结构体大小

以ARM32架构(默认对齐)为例,通过两个经典案例,看成员大小和对齐规则如何共同作用:

案例1:charcharint(成员顺序1)

结构体定义:

struct Example1

{ char a; // 成员1:大小1,对齐值1

char b; // 成员2:大小1,对齐值1

int c; // 成员3:大小4,对齐值4

};

计算步骤:
  1. 分配char a:地址0(1字节),末尾地址0;

  2. 分配char b:下一个地址1(满足对齐值1),占地址1(1字节),末尾地址1;

  3. 分配int c:需对齐值4的地址(4的倍数),当前可用地址2(不满足),填充2字节(地址2-3),c占地址4-7(4字节),末尾地址7;

  4. 整体对齐:最大对齐值=4,总大小7+1=8(8是4的倍数,无需末尾填充);

  5. 最终大小:8字节(成员大小1+1+4=6,填充2字节,共8)。

案例2:charintchar(成员顺序2)

结构体定义:

struct Example2 
{
 char a; // 成员1:大小1,对齐值1 
int b; // 成员2:大小4,对齐值4
 char c; // 成员3:大小1,对齐值1 
};

计算步骤:
  1. 分配char a:地址0(1字节),末尾地址0;

  2. 分配int b:需对齐值4的地址,当前可用地址1(不满足),填充3字节(地址1-3),b占地址4-7(4字节),末尾地址7;

  3. 分配char c:下一个地址8(满足对齐值1),占地址8(1字节),末尾地址8;

  4. 整体对齐:最大对齐值=4,总大小8(不满足4的倍数,8+1=9?不,9不是4的倍数,需填充3字节到12);

  5. 最终大小:12字节(成员大小1+4+1=6,填充3+3=6字节,共12)。

关键对比:成员顺序影响填充字节

两个案例的成员类型和大小完全相同,但总大小差异4字节——核心原因是成员顺序改变了填充字节的位置和数量:

  • 案例1中,两个char紧凑排列,仅需2字节填充;

  • 案例2中,charint交替,需3字节成员间填充+3字节末尾填充,共6字节填充。

五、补充:编译器的“强制对齐”——打破默认规则(#pragma pack

默认对齐规则可通过编译器指令(如#pragma pack(n))强制修改,其中n是“强制对齐值”(如1、2、4),此时对齐规则变为:

  • 成员的自身对齐值 = **min(默认自身对齐值, n)**;

  • 结构体的整体对齐值 = **min(最大默认自身对齐值, n)**。

:对案例2使用#pragma pack(1)(强制1字节对齐):

#pragma pack(1) // 强制1字节对齐,无填充 
struct Example2
 { 
char a; // 地址0 int b; // 地址1(无需填充,因对齐值=1) 
char c; // 地址5 
}; 
#pragma pack() // 恢复默认对齐

总大小=1+4+1=6字节(无任何填充),但代价是int存放在非对齐地址(1-4),ARM32访问时效率降低,甚至可能异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值