字节序描述一个多字节数值的各字节如何按地址或传输顺序排列。这里的“高字节、低字节”指在数值中的权重,与字节本身数值的大小无关,也不是地址高低的同义词。
用一个 32 位数区分两种排列
跳转到“用一个 32 位数区分两种排列”把 0x12345678 放在从地址 A 开始的四个字节中:
| 地址递增顺序 | A | A+1 | A+2 | A+3 |
|---|---|---|---|---|
| 小端 Little-endian | 78 | 56 | 34 | 12 |
| 大端 Big-endian | 12 | 34 | 56 | 78 |
小端把最低有效字节放在最低地址;大端把最高有效字节放在最低地址。无论内存如何排列,数值仍是 0x12345678,通常写出的十六进制数也不会倒着写。
单个字节没有这个多字节排列问题。串行线路先发送哪一位属于位传输顺序,与多字节整数的字节序需要分别规定。字符数组中的字符顺序也不能直接当作整数的字节序。
协议与文件格式应自己规定字节序
跳转到“协议与文件格式应自己规定字节序”网络字节序通常指大端,例如传统 Internet 协议使用的 16 位或 32 位整数转换接口。但具体应用协议、设备帧和文件格式可以另有规定,不能因为“通过网络发送”就自动采用大端。
大端中位于低地址的最高有效字节,对数值的权重反而最大;这不是网络标准选择它的因果解释。对数据交换更有用的规则是:发送方与接收方都按照同一个格式编码、解码。
| 场景 | 应明确的内容 |
|---|---|
| 网络或串口协议 | 每个字段的位宽、符号、字节序与偏移;多寄存器字段还可能另有字序 |
| 二进制文件或数据库记录 | 文件格式规定、版本与字段编码;不能直接保存任意宿主结构体内存 |
| 跨平台交换 | 按外部格式解码成宿主数值,写出时再编码;不依赖两端 CPU 恰好相同 |
| 库接口 | 接口需要的是宿主数值、网络顺序值还是已经编码的字节,避免重复转换 |
htons、htonl 分别用于 16 位、32 位宿主到网络顺序的转换;ntohs、ntohl 做反向转换。这些是网络编程环境提供的接口,不是 ISO C/C++ 本身的一组通用函数。大端宿主上它们可以不需要交换字节,小端宿主上通常需要交换;应用程序应按接口约定调用,而不是自行猜测机器类型。
不依赖宿主字节序的 C 编解码
跳转到“不依赖宿主字节序的 C 编解码”下面以存在 uint8_t、uint32_t 的 C11 环境为例。四字节缓冲区已经由调用方保证有效;若处理外部输入,应先检查剩余长度,再调用解码函数。
#include <assert.h>#include <inttypes.h>#include <stdint.h>#include <stdio.h>#include <string.h>
static uint32_t load_be32(const uint8_t p[4]) { return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3];}
static uint32_t load_le32(const uint8_t p[4]) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);}
static void store_be32(uint8_t p[4], uint32_t value) { p[0] = (uint8_t)(value >> 24); p[1] = (uint8_t)(value >> 16); p[2] = (uint8_t)(value >> 8); p[3] = (uint8_t)value;}
static void store_le32(uint8_t p[4], uint32_t value) { p[0] = (uint8_t)value; p[1] = (uint8_t)(value >> 8); p[2] = (uint8_t)(value >> 16); p[3] = (uint8_t)(value >> 24);}
int main(void) { const uint32_t values[] = {0, UINT32_C(0x12345678), UINT32_MAX}; uint8_t be[4], le[4]; for (size_t n = 0; n < sizeof values / sizeof values[0]; ++n) { store_be32(be, values[n]); store_le32(le, values[n]); assert(load_be32(be) == values[n]); assert(load_le32(le) == values[n]); } store_be32(be, UINT32_C(0x12345678)); store_le32(le, UINT32_C(0x12345678)); const uint8_t expected_be[] = {0x12, 0x34, 0x56, 0x78}; const uint8_t expected_le[] = {0x78, 0x56, 0x34, 0x12}; assert(memcmp(be, expected_be, sizeof be) == 0); assert(memcmp(le, expected_le, sizeof le) == 0); printf("decoded: %08" PRIx32 "\n", load_be32(be)); return 0;}先转成 uint32_t 再左移,避免让有符号 int 的整数提升参与高位移位。代码没有把未对齐的字节指针强转为整数指针,也不依赖结构体填充或宿主对象布局。
架构名称不能代替格式说明
跳转到“架构名称不能代替格式说明”x86 的常用多字节整数内存表示是小端。Arm、MIPS 等架构家族存在不同字节序能力或配置,SPARC 的常见环境使用大端;读设备文档时仍应确认具体处理器、运行状态、ABI 和外部数据格式。不要据某个 RISC 家族名称认定所有设备都采用同一种排列。
- Python struct:用显式格式符控制打包与解包。
- Modbus 寄存器映射:区分单寄存器字节顺序与多寄存器数据布局。