Skip to content

数据对齐与大小端

2026 大纲 四(四)数据的对齐和大/小端存放方式。⚠️ 这一条在考纲里属于第四章 指令系统(字节序与对齐是指令集体系结构的属性),本文因历史原因留在 data-repr 目录下,目录位置不代表大纲归属。

字节序和对齐是两件毫不相干的事

这一节里有两组规则,很多人把它们混在一起想,结果两边都算不对:

🔴 字节序管一个数据"内部"的字节先后(由体系结构决定),对齐管数据"整体"放在哪个地址(由 ABI 规范与编译器决定)。算结构体布局完全用不到字节序,判字节序也不需要对齐规则。

先把两者共用的一个前提说死:

🔴 "数据的地址"指的是它占用的连续单元中最小的那个地址,与字节序无关。字节序决定的只是"这个最小地址上放的是哪一个字节"——大端放最高有效字节(MSB),小端放最低有效字节(LSB)。地址是同一个,内容不同。

一、字节序:多字节数据的排列约定

现代计算机基本都按字节编址,而 int、float 占 4 字节,double 占 8 字节,一个变量要占用连续的若干个存储单元。以 32 位数据 0x12345678 存放在 0x100 起为例:

地址0x1000x1010x1020x103
大端(MSB 在低地址)12345678与人书写顺序一致,即网络字节序(TCP/IP)。代表架构:IBM 360/370、Motorola 68k、MIPS、SPARC
小端(LSB 在低地址)78563412代表架构:Intel 80x86、DEC VAX、ARM(默认)。Alpha、PowerPC 两种都支持,加电启动时选定

判别一台机器字节序的原理是取出多字节数据的第一个字节,看它是高位还是低位

c
int x = 1;                  // 0x00000001
if (*(char *)&x == 1) {
    // 低地址存的是 0x01(LSB),小端
} else {
    // 低地址存的是 0x00(MSB),大端
}

同样的判据也适用于给定的内存转储:把真值按十六进制写出来,再和内存里从低地址到高地址的字节序列逐一对照。从低位开始拆能对上就是小端,从高位开始拆能对上就是大端。

🔴 小端倒着拼,大端顺着拼。 题面给的内存转储是物理顺序(低地址在左),不是数值的可读形式。从低地址到高地址逐字节读出之后,小端必须倒着拼才是真值——顺着读是这类题最主要的失分点。

⚠️ 两点补充:两种字节序没有优劣,只是体系结构设计者的一次性约定,麻烦只出现在跨系统通信时(网络协议钉死大端,由两端各自转换;音视频与图像的文件格式同样要在规范里写死)。另外字节序不只管内存里的数据——机器码中的立即数、位移量字段同样按字节序摆放,所以反汇编时读出的偏移量也要按同一规则拼。

小端有一个实质上的技术优势值得知道:

🔴 变宽度访问同一个数时,小端下低字节的地址不变。 把一个 int 的低 8 位当 char 读,小端下的地址与读 int 时完全相同,硬件不必做地址调整;大端则要把地址加 3。

二、边界对齐:省的是访存次数

存储器不是一个字节一个字节地读的。一个 8 字节宽的存储器,一次访问取出的是地址 8i8i+7 的那一整块。如果一个 4 字节 int 恰好落在第 6、7、8、9 这四个字节上,它跨越了两个块边界,硬件必须访问存储器两次再把结果拼起来。对齐就是保证这种跨越不会发生:代价是内存里出现一些空洞,收益是每个数据都只需一次访存。

最简单的策略:k 字节的数据存放在 k 的整数倍地址上

数据类型大小对齐要求
char1B1(任意地址)
short2B2 的倍数
int / float4B4 的倍数
double8B8 的倍数
数组 T[n]n× sizeof(T)= 元素类型 T 的对齐要求(不是数组总长)
嵌套结构体见第三节= 其内部成员中最大的对齐要求

具体到几字节由 ABI 规定,不是物理定律

上表是最典型的一种策略(Windows 采用),但并非唯一。Linux 遵循的 i386 System V ABI 规定:short 对齐到 2,而 intfloatdouble、指针一律对齐到 4——double 在这种规范下就可能需要两次访存。

所以做布局计算时,题面给定的对齐要求优先;没给定时按"k 字节对齐到 k"这条通用策略走。

一条推论:对齐等价于地址低位恒为 0。"地址是 k 的倍数"(k 为 2 的幂)等价于"地址的二进制低 log2k 位恒为 0":

对齐到2 字节4 字节8 字节
地址低位末 1 位恒为 0末 2 位恒为 00末 3 位恒为 000

既然这几位恒为 0,硬件就可以不存它们。所以"指令按 4 字节对齐、地址空间 32 位"时,PC 只需 30 位。转移指令的偏移量字段同理,可以只存"以指令为单位"的位移量,寻址范围因此扩大 4 倍。

三、结构体的三条对齐规则

结构体由若干基本类型拼成,要保证其中每一个成员都满足自己的对齐要求

① 整个结构体的对齐要求 = 其中对齐要求最严格的那个成员② 每个成员在满足自身对齐要求的前提下,取最小的可用偏移量 —— 可能导致内部插空③ 结构体的总大小必须是它自身对齐要求的整数倍 —— 可能导致尾部插空

前两条保证结构体内部每个成员都对齐,第三条保证结构体数组里的每个元素都对齐——第 i 个元素的首地址是「数组首地址 + i× sizeof(struct)」,步长就是含尾部填充的 sizeof。规则 ③ 用一次填充,换来了数组中所有后续元素的对齐;只定义单个结构体变量时它确实是纯开销,但编译器无法预知你会不会把它放进数组。

套规则之前,有三处判断必须先摆正:

🔴 对齐要求由元素类型决定,不是数组总长。 char name[10] 的对齐要求是 1 不是 10,int arr[100]4 不是 400。判据是:对齐服务于单次访存,而访问数组一次只访问一个元素。把"k 字节对齐到 k"生搬到数组整体上,是结构体布局计算里最主要的错法。

🔴 规则 ① 取的是最大的"对齐要求",不是最大的"大小"。 绝大多数情况下两者相同(基本类型的对齐要求等于其大小),差别只出现在数组成员嵌套结构体成员上:char s[16] 大小 16、对齐要求 1struct{char a; int b;} 大小 8、对齐要求 4

🔴 已知各成员大小之和,算不出 sizeof 成员之间可能插空、末尾可能补齐,sizeof 只增不减,而且增多少取决于成员的排列顺序——同样四个成员,顺序不同可以是 24 与 16 的差别。必须逐成员排偏移,没有捷径。 反过来用:按对齐要求从大到小排列成员,填充最少,因为大对齐的成员先占位时,它后面的小成员总能紧贴着放下。

c
struct S {
    char  a;    // 对齐 1,偏移 0
    int   b;    // 对齐 4 → 偏移 1、2、3 都不可用,落到偏移 4(插 3 字节空)
    short c;    // 对齐 2,偏移 8
};

用到偏移 9 为止,共 10 字节;结构体对齐要求是 4(最严格的成员 int b),10 补齐到 4 的倍数 =12

偏移01234567891011
内容abbbbcc

调整成员顺序可以减少填充:把 int b; short c; char a; 这样从大到小排,用到偏移 6、共 7 字节,补齐到 4 的倍数 =8——仅靠重排就省下 4 字节。

未对齐访问的后果

架构处理方式
x86 / IA-32允许,硬件自动拆成多次访问,只是变慢
ARM(严格模式)产生对齐异常
MIPS必须对齐,否则异常

底层机器级代码本应支持按任意地址访问,所以 IA-32 上不对齐也能正确工作。但操作系统按对齐方式分配内存、编译器按对齐方式生成代码,因为执行效率更高

两道演算:从内存转储反推真值,以及逐成员排出结构体布局(想核对自己拼的方向对不对、偏移是不是一格格排出来的时展开)

(一)字节序与反推。 32 位数据 0xAABBCCDD 存放在地址 0x1000 起:

0x10000x10010x10020x1003
大端AABBCCDD
小端DDCCBBAA

反过来,小端机器上从 0x2000 起连续 4 字节依次是 DC EC FF FF,该 32 位数是多少(按补码解释)?给的是内存物理顺序(低地址在左),小端下低地址存低字节,所以倒着拼

0x2000:DC  0x2001:EC  0x2002:FF  0x2003:FF0xFFFFECDC

按 32 位补码读:最高位为 1,X=0xFFFFECDC232=4900。顺着读成 0xDCECFFFF 是最典型的错法。

(二)逐成员排布。 按"k 字节对齐到 k"计算下列结构体:

c
struct T {
    char   a;
    double b;
    int    c;
    char   d;
};
成员对齐要求偏移占用前面插空
char a100
double b888–151–7(7 字节)
int c41616–19
char d12020

用到偏移 20,共 21 字节;结构体对齐要求 = 8(最严格的 double),补齐到 8 的倍数 =24 字节。

按对齐要求从大到小重排 struct T2 { double b; int c; char a; char d; };

成员对齐要求偏移占用
double b800–7
int c488–11
char a11212
char d11313

用到 14 字节,中间零填充;补齐到 8 的倍数 =16 字节。只改顺序,省下 8 字节。 若把 struct T2 放进数组,sa[i] 的首地址就是 sa + 16i——步长是 16 而不是 14,正是规则 ③ 的作用。

考点速记

  1. 计算机按字节编址,变量地址取所占单元中最小的那个;大端 MSB 在低地址(网络字节序),小端 LSB 在低地址(变宽度访问时低字节地址不变)。两者没有优劣,只在跨系统通信时必须转换,且内存转储要小端倒着拼、大端顺着拼
  2. 对齐省的是访存次数——不对齐的数据会跨越存储器块边界、需要两次访问再拼接;通用策略是 k 字节数据对齐到 k数组按元素类型、嵌套结构体按其内部最严格的成员,具体由 ABI 规定。对齐到 k 等价于地址低 log2k 位恒为 0,这几位可以不存。
  3. 结构体三条规则:① 对齐要求 = 最严格成员的对齐要求(不是最大的大小);② 取最小可用偏移(内部插空);③ 总大小是对齐要求的整数倍(尾部插空)。尾部填充是为结构体数组存在的;按对齐要求从大到小排列成员填充最少。

这一节在真题里被考过的形式(下方「真题练习」里属于本篇的那几道,本章题量第二集中的一篇):

  • 给一个结构体定义与首地址,问某个成员的地址:逐成员排偏移——遇到对齐要求 k 的成员,把当前偏移向上取整到 k 的倍数。⚠️ char name[10] 这类数组成员的对齐要求是元素类型的(1),不是 10。
  • 给一个结构体数组与下标,问某元素某成员的地址:先算出含尾部填充sizeof,再算 首地址 + i × sizeof + 成员偏移。漏掉尾部填充,从第二个元素起全错。
  • 给一个多字节变量的机器数与起始地址,问某个地址上存的是哪个字节小端低地址放最低有效字节——把机器数按字节从低位往高位数过去。这类题几乎不涉及对齐,纯考字节序。
  • 给结构体定义,问按边界对齐分配后某成员的地址:同第一条,只是题面把"边界对齐"四个字明说出来。
  • 给一个 double 变量的地址,问读它要几个存储周期:这一条跨到 多模块存储器——不对齐会多出一次访存,起始地址除以一次并行访问宽度看余数。

易错:把数组成员的对齐要求当成数组总长。看元素类型。

易错:用"各成员大小之和"当 sizeof。必须逐成员排偏移并补尾部。

易错:算结构体数组元素地址时漏掉尾部填充。

易错:内存转储小端顺着拼。要倒着拼。

易错:把字节序牵进结构体布局计算。两件事毫不相干。

教材出处
  • 按字节编址、变量地址取最小首地址、大端/小端的定义与代表架构、跨系统通信需转换字节序:袁春风《计算机组成与系统结构》第 3 版 §2.5.2 数据的存储和排列顺序,印刷页 p46–p47
  • 不对齐导致多次访存的机制(8 字节宽存储器、数据落在第 6~9 字节需访存两次)、Windows 与 i386 System V 两种对齐策略的差异:袁春风《计算机系统基础》§3.5.4 数据的对齐,印刷页 p149
  • 结构体对齐的三条规则,以及第 ③ 条"为保证结构体数组中每个元素都满足对齐要求"的说明与 struct SDT 尾部插空实例:同书 §3.5.4,印刷页 p149–p150

相关知识

C语言中的数据类型与转换定点数编码的转换与书写

真题练习