数据的大小端和对齐存储
前面所有小节讨论的都是一个数由哪些位组成。本节换一个问题:这些位放进内存时,占哪些地址、按什么顺序放。
两件事,互相独立:
- 大小端决定多字节数据的字节次序;
- 边界对齐决定它的起始地址。
它们互不决定,也互不影响——这是本节最常被混掉的一处,必须一开始就分开。
机制
大端与小端
计算机通常按字节编址,一个 32 位整数要占 4 个连续地址。这 4 个字节谁放低地址,有两种约定:
| 定义 | 低地址放的是 | |
|---|---|---|
| 大端(big-endian) | 最高有效字节存在最低地址 | 最高有效字节(MSB) |
| 小端(little-endian) | 最低有效字节存在最低地址 | 最低有效字节(LSB) |
记法:“端”指的是”哪一端放在低地址”。大端 = 大的那一端(高位字节)在前;小端 = 小的那一端(低位字节)在前。
以 0x12345678 存放在起始地址 0x100 为例:
| 地址 | 0x100 | 0x101 | 0x102 | 0x103 |
|---|---|---|---|---|
| 大端 | 12 | 34 | 56 | 78 |
| 小端 | 78 | 56 | 34 | 12 |
大端的字节顺序与人的书写顺序一致(从左到右先写高位),所以看内存转储时读起来顺;小端的优势在于类型转换:把 int 当 short 读时,低地址处正好就是低 16 位,指针不用调整。
| 采用方 | 端序 |
|---|---|
| x86 / x86-64 | 小端 |
| ARM / RISC-V | 可配置,实际几乎都是小端 |
| 网络字节序(TCP/IP) | 大端 |
| PowerPC、SPARC(传统) | 大端 |
“网络字节序是大端”直接导致跨机通信必须做转换,这就是 htons/htonl/ntohs/ntohl 存在的理由:在小端机器上它们真的交换字节,在大端机器上它们什么也不做。
怎么判断本机是大端还是小端
思路是同一块内存用两种类型去读——正是 位型重解释:
int x = 1; /* 0x00000001 */
char *p = (char *)&x;
if (*p == 1) puts("小端"); /* 最低地址是 01 → LSB 在前 */
else puts("大端"); /* 最低地址是 00 → MSB 在前 */关键在于”取地址后按字节读”。若写成 (char)x,那是值转换,两种机器上都得 1,测不出任何东西。
大小端不是什么
这一段是本节的失分重灾区,四条否定要能立刻答出:
① 不改变位序。 大小端只规定字节之间的顺序,一个字节内部的 8 位次序在两种机器上完全一致。位序(bit order)只在串行传输的物理层才有意义。
② 不改变寄存器里的数值。 数据一旦装入寄存器就是一个整体的数,没有”顺序”可言。大小端只在内存与寄存器之间搬运时起作用。
③ 对单字节数据无影响。 char、unsigned char 只占一个地址,无所谓次序。所以字符串在两种机器上的内存布局完全相同——字符串是 char 数组,逐元素存放。
④ 不改变数组元素之间的顺序。 a[0] 永远在 a[1] 的低地址侧。大小端只作用在”一个多字节标量内部”,不作用于元素之间。
这四条合起来给出一个统一判据:大小端只在”一个多字节标量对象的内部”生效。
浮点数的字节序
float、double 同样是多字节标量,遵循与整数完全相同的端序。
1.0f 的位串是 0x3F800000,在小端机器上内存里依次是 00 00 80 3F。IEEE 754 只规定位的含义,不规定字节在内存中的排列,后者由机器的端序决定。
边界对齐
自然对齐:一个
| 类型 | 大小 | 对齐要求 |
|---|---|---|
char | 1 | 任意地址 |
short | 2 | 地址为 2 的倍数 |
int / float | 4 | 地址为 4 的倍数 |
double / 指针(64 位) | 8 | 地址为 8 的倍数 |
判断某地址是否对齐,只看地址的低几位是否为 0:4 字节对齐
为什么必须对齐
原因在存储器的访问粒度:主存虽然按字节编址,但一次访问取出的是一整个存储字(如 4 字节或 8 字节),且这个存储字的边界是固定的。
设存储字长 4 字节,一个 int 存在地址 0x102:
- 它横跨
0x100~0x103和0x104~0x107两个存储字。 - CPU 必须访存两次,再把两半拼接并移位,才能得到这个
int。
代价至少是访存次数翻倍。 而在某些架构(早期 ARM、MIPS、SPARC)上,非对齐访问直接触发异常,根本不允许。x86 允许非对齐访问但会变慢。
flowchart TD subgraph OK["<b>对齐</b>:int 存于 0x104"] direction LR W1["存储字 0x104~0x107<br/><b>一次访存取回</b>"]:::ok end subgraph BAD["<b>不对齐</b>:int 存于 0x102"] direction LR W2["存储字 0x100~0x107<br/><b>两次访存 + 拼接移位</b>"]:::bad end OK ~~~ BAD classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
计算模板:一个数据跨越几个存储字
这是本节唯一的计算题型,步骤固定:
设存储字长
- 求起始所在存储字:
。 - 求结束所在存储字:
。 - 跨越的存储字数 = 差 + 1,访存次数等于它。
例:double(
要访存 3 次;若对齐存放(
结构体的填充
编译器为满足每个成员的对齐要求,会在成员之间插入填充字节;并且整个结构体的大小必须是”最大成员对齐要求”的整数倍(这样数组里的每个元素都仍然对齐)。
struct A { char c; int i; short s; }; /* sizeof == 12 */
struct B { char c; short s; int i; }; /* sizeof == 8 */逐字节展开:
struct A | 偏移 | struct B | 偏移 | |
|---|---|---|---|---|
c | 0 | c | 0 | |
| 填充 3 | 1~3 | 填充 1 | 1 | |
i | 4~7 | s | 2~3 | |
s | 8~9 | i | 4~7 | |
| 尾部填充 2 | 10~11 | —— | ||
| 合计 | 12 | 合计 | 8 |
成员完全相同,只是顺序不同,大小差了 50%。 规律是按对齐要求从大到小排列成员,填充最少。
struct A 尾部那 2 字节填充常被忘记:它存在的理由是让 struct A arr[2] 中的 arr[1].i 仍落在 4 的倍数上。
边界
大小端 vs 边界对齐
| 大小端 | 边界对齐 | |
|---|---|---|
| 管什么 | 多字节标量内部的字节次序 | 数据的起始地址 |
| 影响 | 跨机器数据交换 | 访存次数、是否异常 |
| 谁决定 | CPU 架构(硬件固定) | 编译器(可用 #pragma pack 改) |
| 对单字节数据 | 无影响 | 无影响(任意地址都对齐) |
两者正交:一台小端机器上的数据既可以对齐也可以不对齐;一台大端机器同理。题目把两者放在一起考时,要分别作答,不要互相推导。
对齐是空间换时间
对齐要付出填充字节的代价。在内存紧张或需要精确控制布局的场合(网络协议报文、文件格式、硬件寄存器映射),会用 #pragma pack(1) 强制取消填充,代价是访问变慢或在某些架构上不可用。
这个取舍与本章反复出现的”面积换时间”是同一类判断,只是这次换的是内存空间。
与操作系统内存管理的分界
本节的对齐是编译器在一个数据对象内部做的事,粒度是字节。
操作系统的内存管理关心的是页、段、页框,粒度是 KB。两者层次不同,互不相干:一个 int 是否 4 字节对齐,与它落在哪个页里没有任何关系。
唯一的交叉点是”跨页”:一个数据对象若跨越了页边界,可能需要两次地址转换,甚至触发两次缺页——这与本节”跨存储字要两次访存”结构上相似,但发生在完全不同的层次。
大小端与数据传输的实际影响
只有以字节流形式跨越机器边界时,端序才成为问题:网络传输、二进制文件、共享内存。
用文本格式(JSON、XML)传输就没有端序问题,因为文本是字符序列,属于上面”不改变数组元素顺序”那一条。这也是文本协议在互操作性上的优势之一。
对照速查
0x12345678 存于 0x100 | 0x100 | 0x101 | 0x102 | 0x103 |
|---|---|---|---|---|
| 大端 | 12 | 34 | 56 | 78 |
| 小端 | 78 | 56 | 34 | 12 |
| 判据 | 结论 |
|---|---|
| 大小端改变位序吗 | 不改变 |
大小端影响 char 吗 | 不影响 |
| 大小端影响数组元素顺序吗 | 不影响 |
| 网络字节序 | 大端 |
| x86 | 小端 |
| 跨几个存储字 | |
| 结构体大小 | 是最大成员对齐要求的整数倍 |
考点
- 大端 = 高位字节在低地址;小端 = 低位字节在低地址
- 大小端只作用于多字节标量内部:不改位序、不改数组顺序、不影响
char、不影响寄存器数值 - 网络字节序是大端,x86 是小端
- 判断端序要取地址后按字节读,不能用值转换
- 自然对齐 = 起始地址是数据长度的整数倍
- 不对齐的代价是访存次数增加,部分架构直接异常
- 跨存储字数
- 结构体成员顺序影响
sizeof;按对齐要求从大到小排最省 - 结构体尾部填充是为了数组中下一个元素仍对齐
- 大小端与对齐正交,由架构 / 编译器分别决定
链接
- 🏠 返回总览:计算机组成原理第 2 章:数据的表示和运算总览
- ⬅️ 上一节:2.3.3 C 语言中的浮点数类型
- 📖 名词库:第 2 章名词库