16. 结构体占内存大小是怎么计算的,有哪些原则?
结构体的内存大小计算主要遵循以下原则:
- 每个成员类型的大小:每个成员的内存大小由它的类型决定。例如,
int
通常是 4 字节,char
是 1 字节,double
是 8 字节,等等。 - 成员的对齐要求:系统对不同数据类型有对齐要求。例如,在 32 位系统中,
int
通常要求 4 字节对齐,double
通常要求 8 字节对齐。这意味着该成员的起始地址必须是其大小的整数倍。 - 结构体的对齐要求:结构体的总大小必须是其最大对齐成员的倍数。这通常会导致在结构体末尾添加填充字节,以确保结构体的对齐符合最大成员的对齐要求。
1. 内存对齐原则
每个成员在结构体中的位置,必须满足它自身的对齐要求。编译器可能会在成员之间添加填充字节(padding),以保证每个成员的起始地址满足其对齐要求。
- 对齐要求是指内存地址必须是某个数值的倍数,通常是 2 的幂。
- 最后,结构体的大小需要是最大对齐的倍数。
2. 计算步骤
- 逐一计算每个成员的大小。
- 考虑对齐要求,并在必要时加入填充字节。
- 最后,确保结构体的总大小是对齐要求的倍数。
3. 示例
示例 1:简单结构体
struct S1 {
char c; // 1 字节
int i; // 4 字节
double d; // 8 字节
};
char c
占 1 字节,位于结构体的开始,偏移量为 0。对齐要求为 1 字节,因此无需填充。int i
占 4 字节,要求 4 字节对齐。因为上一个成员char
只占用了 1 字节,因此在int
前需要填充 3 字节(地址 1 到 3),i
位于地址 4 到 7。double d
占 8 字节,要求 8 字节对齐。上一个成员int
占用 4 个字节,因此d
可以直接存储在地址 8 到 15。
总内存布局如下:
字节偏移 | 成员 | 占用大小 |
---|---|---|
0 | char c | 1 字节 |
1 - 3 | 填充 | 3 字节 |
4 - 7 | int i | 4 字节 |
8 - 15 | double d | 8 字节 |
- 总大小为 16 字节。
- 由于
double
是对齐要求最大的成员,结构体的总大小必须是 8 的倍数,所以没有额外的填充字节。
示例 2:更复杂的结构体
struct S2 {
char c1; // 1 字节
char c2; // 1 字节
double d; // 8 字节
int i; // 4 字节
};
char c1
占 1 字节,偏移量 0。char c2
紧接着c1
,占 1 字节,偏移量 1。double d
要求 8 字节对齐,但上一个成员char c2
的地址为 1,因此必须插入 6 字节的填充,使d
的起始地址为 8。int i
要求 4 字节对齐,d
占用了 8 到 15 字节,因此i
从 16 开始,位于 16 到 19 字节。
内存布局如下:
字节偏移 | 成员 | 占用大小 |
---|---|---|
0 | char c1 | 1 字节 |
1 | char c2 | 1 字节 |
2 - 7 | 填充 | 6 字节 |
8 - 15 | double d | 8 字节 |
16 - 19 | int i | 4 字节 |
- 目前为止占用了 20 字节。
- 最大对齐单位是
double
(8 字节),因此结构体的大小必须是 8 的倍数,最后需要添加 4 字节的填充,使总大小达到 24 字节。
示例 3:位域的结构体
struct S3 {
int a : 4; // 位域,占用 4 位(0.5 字节)
int b : 4; // 位域,占用 4 位(0.5 字节),可以与 a 共用一个字节
int c : 8; // 位域,占用 8 位(1 字节)
double d; // 普通成员,占用 8 字节
};
a
和b
可以共享一个字节,存储在字节 0。c
占用 1 字节,存储在字节 1。double d
需要 8 字节对齐,因此需要从字节 8 开始,因此会有 6 字节的填充。
内存布局如下:
字节偏移 | 成员 | 占用大小 |
---|---|---|
0 | a:4 + b:4 | 1 字节 |
1 | c:8 | 1 字节 |
2 - 7 | 填充 | 6 字节 |
8 - 15 | double d | 8 字节 |
- 总大小为 16 字节。
4. 总结计算原则
- 逐一计算每个成员的大小和对齐要求。
- 对齐补齐:如果一个成员的起始地址不满足对齐要求,则需要填充字节来对齐它。
- 结构体对齐:结构体的总大小应是其最大成员的对齐要求的倍数,因此需要在结构体末尾填充字节。
5. 注意点
- 平台相关性:不同平台对不同类型的对齐要求可能不同,因此同一个结构体在不同平台上的大小可能不同。
- 位域的实现依赖于编译器:位域的存储和对齐可能因编译器的不同而不同,位域的使用需要考虑到目标平台的实现细节。