一、基础:成员类型大小——结构体大小的“计算起点”
结构体的本质是“多个不同类型数据的集合”,每个成员的自身大小是计算结构体总大小的“原始素材”,由编程语言和硬件架构共同定义(以主流的32/64位架构为例):
数据类型 |
32位架构(如ARM32) |
64位架构(如ARM64/x86_64) |
说明 |
|
1字节(8位) |
1字节(8位) |
最小整数类型,无符号/有符号 |
|
2字节(16位) |
2字节(16位) |
短整数 |
|
4字节(32位) |
4字节(32位) |
标准整数(兼容32位操作) |
|
4字节(32位) |
8字节(64位) |
架构差异核心点 |
|
8字节(64位) |
8字节(64位) |
64位整数 |
|
4字节(32位) |
4字节(32位) |
单精度浮点数 |
|
8字节(64位) |
8字节(64位) |
双精度浮点数 |
指针( |
4字节(32位地址) |
8字节(64位地址) |
指向内存地址的变量 |
关键结论:
每个成员的大小是固定的(如char
恒为1字节),但结构体总大小不是成员大小的简单相加——因为内存对齐规则会插入“填充字节”,这是理解结构体大小的核心。
二、核心:内存对齐规则——为什么需要对齐?(硬件层面的底层逻辑)
内存对齐的本质是硬件访问效率的妥协:
计算机的CPU访问内存时,并非“按字节逐个读取”,而是按“固定块大小”(如ARM32的4字节、ARM64的8字节)批量读取,如下图,每一次读取都是读取四个字节。
若数据存放在“非对齐地址”(如4字节的int
存放在地址1-4),CPU需要:
-
先读取地址0-3的块,提取其中的1-3字节;
-
再读取地址4-7的块,提取其中的4字节;
-
最后拼接这两部分数据——额外增加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:char
、char
、int
(成员顺序1)
结构体定义:
struct Example1
{ char a; // 成员1:大小1,对齐值1
char b; // 成员2:大小1,对齐值1
int c; // 成员3:大小4,对齐值4
};
计算步骤:
-
分配
char a
:地址0(1字节),末尾地址0; -
分配
char b
:下一个地址1(满足对齐值1),占地址1(1字节),末尾地址1; -
分配
int c
:需对齐值4的地址(4的倍数),当前可用地址2(不满足),填充2字节(地址2-3),c
占地址4-7(4字节),末尾地址7; -
整体对齐:最大对齐值=4,总大小7+1=8(8是4的倍数,无需末尾填充);
-
最终大小:8字节(成员大小1+1+4=6,填充2字节,共8)。
案例2:char
、int
、char
(成员顺序2)
结构体定义:
struct Example2
{
char a; // 成员1:大小1,对齐值1
int b; // 成员2:大小4,对齐值4
char c; // 成员3:大小1,对齐值1
};
计算步骤:
-
分配
char a
:地址0(1字节),末尾地址0; -
分配
int b
:需对齐值4的地址,当前可用地址1(不满足),填充3字节(地址1-3),b
占地址4-7(4字节),末尾地址7; -
分配
char c
:下一个地址8(满足对齐值1),占地址8(1字节),末尾地址8; -
整体对齐:最大对齐值=4,总大小8(不满足4的倍数,8+1=9?不,9不是4的倍数,需填充3字节到12);
-
最终大小:12字节(成员大小1+4+1=6,填充3+3=6字节,共12)。
关键对比:成员顺序影响填充字节
两个案例的成员类型和大小完全相同,但总大小差异4字节——核心原因是成员顺序改变了填充字节的位置和数量:
-
案例1中,两个
char
紧凑排列,仅需2字节填充; -
案例2中,
char
和int
交替,需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访问时效率降低,甚至可能异常。