C 结构体的内存布局、对齐与填充
通过具体布局理解 C 结构体的对齐、填充、字段偏移与数组排列。
建议先了解
- C 基本类型
- 指针基础
1. 这篇笔记解决什么问题
本篇回答 C struct 在真实内存里是如何组织的,以及为什么理解结构体布局是继续阅读 CPython 对象结构的前提。
学完后应能解释:成员是否连续;为什么出现 padding;为什么字段顺序改变 sizeof;指针成员和内嵌数组有什么区别;p.x 为什么可以被编译成“基地址 + offset”的访问。
2. 前置知识
- byte、地址、指针;
- C 基本数据类型。
待补充:ABI 对类型大小、对齐和调用约定的具体约束。
3. 核心概念
3.1 结构体是一段有固定布局的内存
struct A {
int x;
int y;
int z;
};
在常见 int=4B 的平台上,概念布局:
offset 0 4 8 12
┌──────┬──────┬──────┐
│ x │ y │ z │
└──────┴──────┴──────┘
成员按照声明顺序排列,但中间可能插入 padding。
3.2 对齐(Alignment)
某些类型通常希望从满足特定边界的地址开始。
常见示意:
char size 1 alignment 1
short size 2 alignment 2
int size 4 alignment 4
double size 8 alignment 8
pointer size 8 alignment 8
[!WARNING] 这些值依赖 CPU/ABI/编译器目标,不能当成 C 语言跨平台固定保证。
3.3 填充(Padding)
struct A {
char c;
int x;
};
常见布局:
0 1 2 3 4 8
┌───────┬─────────────┬──────────────┐
│ c │ padding │ x │
└───────┴─────────────┴──────────────┘
1 B 3 B 4 B
因此结构体可能是 8B,而不是 5B。
3.4 尾部填充
struct B {
int x;
char c;
};
成员有效数据可能只有 5B,但结构体总大小仍可能补到 8B。原因之一是保证:
struct B arr[100];
数组中每个 arr[i] 的起始地址都满足结构体整体对齐要求。
3.5 成员偏移(Offset)
编译器在编译时可以知道:
x 在结构体起始地址后的多少字节
所以:
p.x
概念上接近:
address(p) + offset(x)
再对相应内存进行 load/store。
4. 直观理解
结构体可以想成一张“固定格式记录”:字段名只存在于源码/调试/符号层面,真正执行时关键是结构体起始地址与成员 offset。
类比边界:编译器优化后实际机器码不一定逐字面执行一次“加法再访问”,但地址计算语义等价。
5. 工作原理
5.1 字段顺序会影响总大小
struct A {
char a;
double b;
char c;
};
在常见 8B 对齐的 double 平台上,可能出现:
a + 7B padding + b + c + 7B padding
≈ 24B
若重排:
struct B {
double b;
char a;
char c;
};
可能为:
b + a + c + 6B padding
≈ 16B
这说明同样字段集合,不同排列可能导致不同 padding。
5.2 指针成员与内嵌数组
指针:
struct Person {
int age;
char *name;
};
结构体内主要保存 name 的地址:
struct Person
┌────────────┐
│ age │
├────────────┤
│ name ptr ──┼────→ 另一处字符串数据
└────────────┘
数组:
struct Person {
int age;
char name[20];
};
20B 数组直接嵌在结构体中。
5.3 与 CPython 的关系
CPython 的大量对象底层就是 C 结构体。
对话中曾简化理解 Python 对象头为:
PyObject
├── 引用管理信息
└── 指向类型对象的指针
整数对象再在基础对象头后增加整数自身数据。
因此理解 struct + offset + pointer 是继续读 PyObject、PyLongObject 源码的基础。
6. 示例
示例:用 offsetof 验证
#include <stdio.h>
#include <stddef.h>
struct A {
char c;
int x;
double y;
};
int main(void) {
printf("sizeof(A) = %zu\n", sizeof(struct A));
printf("c = %zu\n", offsetof(struct A, c));
printf("x = %zu\n", offsetof(struct A, x));
printf("y = %zu\n", offsetof(struct A, y));
}
输入:本机编译器和 ABI。
输出:结构体总大小和字段 offset。
说明:可直接观察 padding 对布局的影响。
7. 代码、命令或公式
若:
则:
struct A arr[3];
三个元素起始地址概念上为:
8. 容易混淆的概念
| 概念 A | 概念 B | 核心区别 |
|---|---|---|
| 字段大小 | 字段 offset | offset 还受前面字段和 padding 影响 |
| padding | 有效数据 | padding 为布局/对齐服务,不是业务字段 |
char *name | char name[20] | 前者存地址,后者直接嵌入数据 |
| struct 对象 | struct 指针 | 前者是完整内存块,后者只是地址 |
| C 成员访问 | Python 动态属性访问 | C offset 通常编译期确定,Python 属性查找更动态 |
9. 常见误区
误区:sizeof(struct) 就是所有字段大小直接相加
错误原因:忽略 alignment 和 padding。
正确理解:编译器可能插入成员间填充和尾部填充。
如何验证:使用 sizeof 和 offsetof。
误区:char *name 把字符串内容放进结构体
错误原因:混淆指针与数组。
正确理解:结构体只保存指针值,字符串内容通常位于其他内存区域。
误区:所有 64 位平台结构体布局都一样
错误原因:忽略 ABI。
正确理解:必须以目标平台 ABI 或实际编译实验为准。
10. 与其他知识的关系
待建链接:ABI 与调用约定
11. 可以亲手完成的验证
目标:验证字段重排对结构体大小的影响。
struct A {
char a;
double b;
char c;
};
struct B {
double b;
char a;
char c;
};
打印两者 sizeof 和各字段 offsetof。
预期:常见 ABI 下可能出现不同总大小和 padding。
实验不能证明:本机结果不能推广为所有 C 实现的固定规则。
12. 尚未解决的问题
- C 标准与 ABI 分别保证哪些 struct layout 行为未系统区分。
#pragma pack、packed struct 和未对齐访问风险未讨论。- bit-field 布局未展开。
- cache line、false sharing 与字段布局性能关系未展开。
13. 自测问题
- 为什么
char + int的结构体可能不是 5B? - 为什么结构体末尾也需要 padding?
char *name与char name[20]的内存含义有什么区别?- 编译器为什么可以直接用固定 offset 访问成员?
- 为什么字段顺序会影响
sizeof?
参考答案
int可能要求按特定边界对齐,因此char后会补 padding。- 为保证结构体数组中下一个元素仍满足整体对齐。
- 指针字段只存地址;数组字段的数据直接属于结构体本身。
- 结构体类型和 ABI 在编译时确定成员相对起始地址的 offset。
- 不同排列会产生不同数量的成员间和尾部 padding。
14. 一句话总结
C struct 是一段受 ABI 对齐规则约束的固定内存布局;成员 offset、padding、指针与内嵌数据的区别,是理解 CPython 对象和系统内存结构的直接基础。