跳转到内容
新建笔记

C结构体对齐问题

“结构体大小的计算遵循内存对齐规则,不是简单地把所有成员大小相加。主要涉及三个原则:起始对齐、成员对齐、整体对齐。”


“结构体的起始地址必须是最大成员大小的整数倍(或对齐数的整数倍)。”

“每个成员的偏移量必须是该成员自身大小的整数倍(或对齐数的整数倍)。

  • 如果不满足,需要在前面填充字节使其满足对齐要求。”

“结构体的总大小必须是最大成员大小的整数倍(或对齐数的整数倍)。

  • 如果不满足,需要在末尾填充字节。”
struct A {
char a; // 1字节
int b; // 4字节
char c; // 1字节
};

计算过程:

成员大小自身对齐要求偏移量说明
char a1字节1字节0起始位置
(填充)3字节-1-3int需要4字节对齐
int b4字节4字节4满足4字节对齐
char c1字节1字节8紧接着int
(填充)3字节-9-11总大小需是4的倍数

结果:sizeof(struct A) = 12字节

内存布局:
[a][ 填充 ][ b ][c][ 填充 ]
0 1 2 3 4 5 6 7 8 9 10 11

结构体内存对齐详细讲解

跳转到“结构体内存对齐详细讲解”

让我一步一步详细解释这个例子,帮你彻底理解内存对齐的原理。


📚 前置知识:为什么需要内存对齐?

跳转到“📚 前置知识:为什么需要内存对齐?”

现代CPU访问内存不是一个字节一个字节读的,而是按块读取:

  • 32位系统:一次读取4字节(一个字长)
  • 64位系统:一次读取8字节(一个字长)

场景1:int对齐访问(高效)

内存地址: 0 1 2 3 4 5 6 7
内存内容: [————int————] [————int————]
一次读取OK! 一次读取OK!

CPU只需一次读取就能拿到完整的int。

场景2:int未对齐访问(低效)

内存地址: 0 1 2 3 4 5 6 7
内存内容: [a][——int——][——] [——int——]
↑需要读两次!
第一次读0-3 第二次读4-7
然后拼接、位移

CPU需要:

  1. 读取地址0-3(拿到int的前3字节)
  2. 读取地址4-7(拿到int的后1字节)
  3. 通过位运算拼接成完整int

效率差距:对齐访问比未对齐快2-3倍!


🔍 逐步分析:struct A 的计算过程

跳转到“🔍 逐步分析:struct A 的计算过程”
struct A {
char a; // 1字节
int b; // 4字节
char c; // 1字节
};

Step 1: 放置第一个成员 char a

跳转到“Step 1: 放置第一个成员 char a”
内存地址: 0 1 2 3 4 5 6 7 8 9 10 11
内存内容: [a]
↑
偏移0

规则:

  • char大小是1字节,对齐要求是1字节
  • 起始位置偏移0,满足对齐要求 ✅
  • 当前已用空间:1字节

Step 2: 放置第二个成员 int b

跳转到“Step 2: 放置第二个成员 int b”

关键:int需要4字节对齐!

如果直接放在偏移1:
内存地址: 0 1 2 3 4 5 6 7
内存内容: [a][——int b——]
↑ 偏移1

问题分析:

  • 偏移1 ÷ 4 = 0.25(不是整数)❌
  • 1不是4的倍数,违反对齐规则!
  • CPU访问这个int需要读两次内存

解决方案:填充字节

内存地址: 0 1 2 3 4 5 6 7 8
内存内容: [a][padding][——int b——]
↑ ↑不用,浪费
偏移0 偏移1-3 偏移4

填充计算:

  • int b需要放在4的倍数位置
  • 下一个4的倍数是:4
  • 需要填充:4 - 1 = 3字节
  • 填充后偏移:0(a) + 1(a大小) + 3(填充) = 4
内存布局:
地址: 0 1 2 3 4 5 6 7
内容: [a][ 填充3字节 ][————int b————]
偏移: 0 1 2 3 4 5 6 7

当前已用空间:1 + 3 + 4 = 8字节


Step 3: 放置第三个成员 char c

跳转到“Step 3: 放置第三个成员 char c”
内存地址: 0 1 2 3 4 5 6 7 8 9 10 11
内存内容: [a][ 填充 ][————int b————][c]
↑
偏移8

规则:

  • char大小1字节,对齐要求1字节
  • 前面用了8字节,偏移8满足对齐 ✅
  • 当前已用空间:8 + 1 = 9字节

Step 4: 整体对齐(关键!)

跳转到“Step 4: 整体对齐(关键!)”

规则:结构体总大小必须是最大成员的整数倍

分析:

  • 最大成员:int b(4字节)
  • 当前大小:9字节
  • 9 ÷ 4 = 2.25(不是整数)❌
  • 需要补齐到4的倍数

计算:

  • 下一个4的倍数:12
  • 需要填充:12 - 9 = 3字节
最终内存布局:
地址: 0 1 2 3 4 5 6 7 8 9 10 11
内容: [a][ 填充 ][————int b————][c][ 填充 ]
偏移: 0 1-3 4-7 8 9-11
大小: 1 3 4 1 3

最终大小:sizeof(struct A) = 12字节


🎯 为什么末尾要填充?

跳转到“🎯 为什么末尾要填充?”

这是很多人的疑问!我用一个数组例子说明:

struct A arr[2]; // 创建结构体数组

如果不补齐(假设9字节):

内存布局:
arr[0]: [a][填充][int b][c]
0 1-3 4-7 8
arr[1]: [a][填充][int b][c]
9 10-12 13-16 17
↑
问题:arr[1]的int b在偏移13
13不是4的倍数!破坏对齐!

补齐到12字节后:

arr[0]: [a][填充][int b][c][填充]
0 1-3 4-7 8 9-11
arr[1]: [a][填充][int b][c][填充]
12 13-15 16-19 20 21-23
↑
✅ 偏移16是4的倍数,满足对齐!

结论:末尾填充保证数组中每个元素的对齐要求!


字节地址: 0 1 2 3 4 5 6 7 8 9 10 11
-----------------------------------------------------------------
内容: a X X X b0 b1 b2 b3 c X X X
-----------------------------------------------------------------
说明: ↑ 填充空间 ↑————int b的4字节————↑ 填充空间
1字节 1字节
图例:
a, c = 实际存储的char数据
b0-b3 = int b的4个字节
X = 填充字节(padding)
4字节块: [———— 块0 ————] [———— 块1 ————] [———— 块2 ————]
地址: 0 1 2 3 4 5 6 7 8 9 10 11
内容: [a 填充填充填充] [b0 b1 b2 b3] [c 填充填充填充]
↑ char ↑ int 完美对齐 ↑ char
满足对齐 一次读取 满足对齐

CPU访问效率:

  • 读取a:一次内存操作(地址0-3)
  • 读取b:一次内存操作(地址4-7)✅ 高效!
  • 读取c:一次内存操作(地址8-11)

💡 对齐规则总结(记忆口诀)

跳转到“💡 对齐规则总结(记忆口诀)”
1. 成员放置看自己:每个成员的偏移必须是自己大小的倍数
- char: 任意位置(1的倍数)
- short: 2的倍数位置(0, 2, 4, 6...)
- int: 4的倍数位置(0, 4, 8, 12...)
- double: 8的倍数位置(0, 8, 16...)
2. 不够就填充:不满足就在前面填充空字节
3. 末尾看最大:总大小必须是最大成员的倍数
- 找出最大成员(这里是int,4字节)
- 总大小必须是4的倍数
- 不够就在末尾填充

#include <stdio.h>
#include <stddef.h>
struct A {
char a;
int b;
char c;
};
int main() {
struct A test;
printf("结构体总大小: %zu 字节\n", sizeof(struct A));
printf("\n各成员偏移量:\n");
printf("char a 偏移: %zu\n", offsetof(struct A, a));
printf("int b 偏移: %zu\n", offsetof(struct A, b));
printf("char c 偏移: %zu\n", offsetof(struct A, c));
printf("\n内存地址验证:\n");
printf("结构体起始地址: %p\n", (void*)&test);
printf("a的地址: %p (偏移 %td)\n", (void*)&test.a,
(char*)&test.a - (char*)&test);
printf("b的地址: %p (偏移 %td)\n", (void*)&test.b,
(char*)&test.b - (char*)&test);
printf("c的地址: %p (偏移 %td)\n", (void*)&test.c,
(char*)&test.c - (char*)&test);
return 0;
}

运行结果:

结构体总大小: 12 字节
各成员偏移量:
char a 偏移: 0
int b 偏移: 4
char c 偏移: 8
内存地址验证:
结构体起始地址: 0x7ffeeb8a9a8c
a的地址: 0x7ffeeb8a9a8c (偏移 0)
b的地址: 0x7ffeeb8a9a90 (偏移 4)
c的地址: 0x7ffeeb8a9a94 (偏移 8)

❌ 错误理解1:简单相加

跳转到“❌ 错误理解1:简单相加”
错误计算:1(a) + 4(b) + 1(c) = 6字节 ❌
实际大小:12字节

❌ 错误理解2:忘记末尾填充

跳转到“❌ 错误理解2:忘记末尾填充”
错误计算:1(a) + 3(填充) + 4(b) + 1(c) = 9字节 ❌
实际大小:12字节(还需补3字节)
正确计算:
- a占用:1字节
- 填充:3字节(让b对齐到4)
- b占用:4字节
- c占用:1字节
- 填充:3字节(让总大小是4的倍数)
= 1 + 3 + 4 + 1 + 3 = 12字节 ✅

把struct A重新排列可以节省空间:

// 原始版本:12字节
struct A {
char a; // 偏移0
int b; // 偏移4(填充3字节)
char c; // 偏移8(末尾填充3字节)
};
// 优化版本:8字节
struct A_optimized {
int b; // 偏移0
char a; // 偏移4
char c; // 偏移5(末尾填充2字节)
};

内存布局对比:

原始版本(12字节):
[a][XXX][bbbb][c][XXX]
优化版本(8字节):
[bbbb][a][c][XX]
节省:12 - 8 = 4字节 (33%的空间!)

“偏移量必须是成员自身大小的整数倍” 详细解析

跳转到““偏移量必须是成员自身大小的整数倍” 详细解析”

让我用 struct A 的例子,逐个成员详细说明这个规则如何体现。


核心规则:每个成员的偏移量必须是该成员自身大小的整数倍

数学表达式:

偏移量 % 成员大小 == 0

struct A {
char a; // 1字节
int b; // 4字节
char c; // 1字节
};

对齐要求:偏移量必须是 1 的整数倍

实际情况:

偏移量 = 0
验证:0 % 1 = 0 ✅ 满足!

说明:

  • char 大小是 1 字节
  • 0 是 1 的整数倍(0 = 1 × 0)
  • 所以 char a 可以放在偏移 0

任何位置都满足:

0 % 1 = 0 ✅
1 % 1 = 0 ✅
2 % 1 = 0 ✅
3 % 1 = 0 ✅
...任意偏移都是1的倍数

对齐要求:偏移量必须是 4 的整数倍

如果直接放在偏移1(错误):

假设偏移 = 1
验证:1 % 4 = 1 ❌ 不满足!
1 不是 4 的整数倍
1 ÷ 4 = 0.25(不是整数)

内存布局示意:

地址: 0 1 2 3 4 5 6 7
[a] [————假设int在这里————]
↑ 偏移1
1 % 4 = 1 ❌ 违反对齐规则!

正确做法:填充到偏移4

偏移 = 4
验证:4 % 4 = 0 ✅ 满足!
4 是 4 的整数倍
4 ÷ 4 = 1(整数)

内存布局:

地址: 0 1 2 3 4 5 6 7
[a][填充填充填充][————int b————]
↑ 偏移4
4 % 4 = 0 ✅

为什么填充到4而不是8?

检查可能的偏移:
1 % 4 = 1 ❌
2 % 4 = 2 ❌
3 % 4 = 3 ❌
4 % 4 = 0 ✅ 这是第一个满足的位置!
所以选择偏移4,填充3字节

对齐要求:偏移量必须是 1 的整数倍

实际情况:

偏移量 = 8(紧跟在int b后面)
验证:8 % 1 = 0 ✅ 满足!

说明:

  • 前面已用:4(int b的偏移) + 4(int b的大小) = 8
  • 8 是 1 的整数倍
  • char c 可以直接放在偏移 8

成员大小实际偏移对齐要求验证计算结果
char a1字节0偏移 % 1 == 00 % 1 = 0✅
int b4字节4偏移 % 4 == 04 % 4 = 0✅
char c1字节8偏移 % 1 == 08 % 1 = 0✅

所有成员都满足对齐要求!


例子1:如果把int b放在偏移1(错误)

跳转到“例子1:如果把int b放在偏移1(错误)”
// 假设的错误布局
地址: 0 1 2 3 4 5
[a][————int b————][...]
↑ 偏移1
验证:
char a: 偏移0, 0 % 1 = 0 ✅
int b: 偏移1, 1 % 4 = 1 ❌ 不是4的倍数!

为什么不行?

1 ÷ 4 = 0.25
1不是4的整数倍
违反对齐规则!

例子2:如果把int b放在偏移2(错误)

跳转到“例子2:如果把int b放在偏移2(错误)”
地址: 0 1 2 3 4 5 6
[a][X][————int b————][...]
↑ 偏移2
验证:
int b: 偏移2, 2 % 4 = 2 ❌ 不是4的倍数!

为什么不行?

2 ÷ 4 = 0.5
2不是4的整数倍
违反对齐规则!

例子3:如果把int b放在偏移8(可以,但浪费)

跳转到“例子3:如果把int b放在偏移8(可以,但浪费)”
地址: 0 1 2 3 4 5 6 7 8 9 10 11
[a][填充填充填充填充填充填充填充][————int b————]
↑ 偏移8
验证:
int b: 偏移8, 8 % 4 = 0 ✅ 满足!

虽然满足对齐,但浪费空间:

偏移4也满足:4 % 4 = 0 ✅
编译器会选择最近的满足对齐的位置
所以选择偏移4,而不是偏移8

偏移量 = n × 成员大小 (n是非负整数)

对于int(4字节):

有效偏移:
0 = 0 × 4 ✅
4 = 1 × 4 ✅
8 = 2 × 4 ✅
12 = 3 × 4 ✅
16 = 4 × 4 ✅
...
无效偏移:
1 (不是4的倍数) ❌
2 (不是4的倍数) ❌
3 (不是4的倍数) ❌
5 (不是4的倍数) ❌
...

🖥️ CPU视角:为什么要这样对齐?

跳转到“🖥️ CPU视角:为什么要这样对齐?”

32位系统一次读4字节的场景

跳转到“32位系统一次读4字节的场景”

场景1:int在偏移4(对齐)

内存地址: 0 1 2 3 | 4 5 6 7 | 8 9 10 11
内存内容:[a][填充填充填充] | [————int b————] | [c][填充填充填充]
CPU读取: [——第1次读取——] | [——第2次读取——] | [——第3次读取——]
↑ int完整在一次读取中!

CPU操作:

读取int b:
1. CPU读取地址4-7(一次内存操作)
2. 直接得到完整的int ✅ 高效!

场景2:int在偏移1(未对齐)

内存地址: 0 1 2 3 | 4 5 6 7 | 8
内存内容:[a][——int的前3字节——] | [int最后1字节][...]
CPU读取: [——第1次读取——] | [——第2次读取——]
↑ int跨越了两次读取!

CPU操作(复杂且慢):

读取int b:
1. CPU读取地址0-3(第一次)→ 得到int的3个字节
2. CPU读取地址4-7(第二次)→ 得到int的1个字节
3. 通过位运算拼接:
结果 = (第一次的后3字节 << 8) | 第二次的第1字节
4. 才得到完整的int ❌ 慢而且复杂!

效率对比:

对齐访问: 1次内存读取
未对齐访问: 2次内存读取 + 位运算
性能差距: 2-3倍!

char随意放(1的倍数随便满足)
short双数位(0, 2, 4, 6...)
int找4倍(0, 4, 8, 12...)
double要8倍(0, 8, 16...)
1. 看成员大小(如int是4)
2. 找当前位置
3. 做除法:位置 ÷ 大小
4. 能整除?可以放!
不能整除?填充到下一个倍数!
struct Example {
char a; // 偏移0: 0÷1=0 ✅
short b; // 偏移2: 2÷2=1 ✅ (填充1字节)
int c; // 偏移4: 4÷4=1 ✅ (不用填充)
char d; // 偏移8: 8÷1=8 ✅
double e; // 偏移16: 16÷8=2 ✅ (填充7字节)
};

#include <stdio.h>
#include <stddef.h>
struct A {
char a;
int b;
char c;
};
int main() {
printf("=== 对齐验证 ===\n\n");
// 成员1: char a
size_t offset_a = offsetof(struct A, a);
printf("char a:\n");
printf(" 大小: 1字节\n");
printf(" 偏移: %zu\n", offset_a);
printf(" 验证: %zu %% 1 = %zu %s\n",
offset_a, offset_a % 1,
(offset_a % 1 == 0) ? "✅" : "❌");
// 成员2: int b
size_t offset_b = offsetof(struct A, b);
printf("\nint b:\n");
printf(" 大小: 4字节\n");
printf(" 偏移: %zu\n", offset_b);
printf(" 验证: %zu %% 4 = %zu %s\n",
offset_b, offset_b % 4,
(offset_b % 4 == 0) ? "✅" : "❌");
// 成员3: char c
size_t offset_c = offsetof(struct A, c);
printf("\nchar c:\n");
printf(" 大小: 1字节\n");
printf(" 偏移: %zu\n", offset_c);
printf(" 验证: %zu %% 1 = %zu %s\n",
offset_c, offset_c % 1,
(offset_c % 1 == 0) ? "✅" : "❌");
printf("\n结构体总大小: %zu字节\n", sizeof(struct A));
return 0;
}

输出结果:

=== 对齐验证 ===
char a:
大小: 1字节
偏移: 0
验证: 0 % 1 = 0 ✅
int b:
大小: 4字节
偏移: 4
验证: 4 % 4 = 0 ✅
char c:
大小: 1字节
偏移: 8
验证: 8 % 1 = 0 ✅
结构体总大小: 12字节

“偏移量必须是成员自身大小的整数倍”在struct A中的体现:

  1. char a (1字节):偏移0,0 % 1 = 0 ✅
  2. int b (4字节):偏移4,4 % 4 = 0 ✅(填充了3字节才满足)
  3. char c (1字节):偏移8,8 % 1 = 0 ✅

核心理解:

  • 编译器会自动插入填充字节
  • 保证每个成员的偏移量满足对齐要求
  • 目的是让CPU能高效访问内存

现在清楚了吗?关键就是:偏移量 % 成员大小 = 0!


来源:CSDN 原文,首次发布于 2025-10-27。