大小端(Endianness)
小端(Little-Endian):低位字节存放在低地址,高位字节存放在高地址。例如0x12345678。常见于x86/x64架构。大端(Big-Endian):高位字节存放在低地址,低位字节存放在高地址。例如0x12345678。常见于某些网络协议(网络字节序)、历史上的某些 RISC 架构等。记忆窍门:小端“把小的字节放前面”(低地址);大端“把大的字节放前面”(低地址)。网络通信:始终使用。
1. 什么是大小端?
-
小端(Little-Endian):低位字节存放在低地址,高位字节存放在高地址。
例如0x12345678在内存中的顺序(从低地址到高地址)是:78 56 34 12。
常见于 x86/x64 架构。 -
大端(Big-Endian):高位字节存放在低地址,低位字节存放在高地址。
例如0x12345678在内存中的顺序是:12 34 56 78。
常见于某些 网络协议(网络字节序)、历史上的某些 RISC 架构等。
记忆窍门:小端“把小的字节放前面”(低地址);大端“把大的字节放前面”(低地址)。
2. 为什么会有大小端?
- 硬件设计历史与指令效率的权衡导致不同 CPU 架构选择不同的字节序。
- 网络协议统一为大端(称为 网络字节序),便于不同平台互联互通。
- 某些架构是 双端(bi-endian)或可配置:如部分 ARM(AArch32)可切换字节序,但 AArch64 基本以小端为主。
3. 在 C 语言中如何检测本机字节序?
方法一:使用联合体(union)
#include <stdio.h>
#include <stdint.h>
int main(void) {
union {
uint32_t i;
uint8_t b[4];
} u = { .i = 0x01020304 };
if (u.b[0] == 0x04) {
puts("Little-Endian");
} else if (u.b[0] == 0x01) {
puts("Big-Endian");
} else {
puts("Unknown / Mixed?");
}
return 0;
}
方法二:指针转换
#include <stdio.h>
#include <stdint.h>
int main(void) {
uint32_t x = 0x01020304;
uint8_t *p = (uint8_t*)&x;
printf("%s\n", (*p == 0x04) ? "Little-Endian" : "Big-Endian");
return 0;
}
注意:严格别名规则在这两种方式下都可安全使用,因为都是以字符类型访问对象,C 标准允许。
4. 与网络编程相关(网络字节序 = 大端)
在套接字编程中,使用标准转换函数实现 主机字节序 ↔ 网络字节序:
- 16 位:
htons(host to network short),ntohs - 32 位:
htonl(host to network long),ntohl
#include <arpa/inet.h>
#include <stdint.h>
#include <stdio.h>
int main(void) {
uint32_t host = 0x12345678;
uint32_t net = htonl(host);
printf("host=0x%08x, net=0x%08x\n", host, net);
return 0;
}
必须使用这些函数,不要自己判断平台然后条件编译,因为这些函数已为你处理好所有平台细节。
5. 常见坑与最佳实践
5.1 结构体打包/网络传输
- 不要直接把结构体用
send()发出去或者用fwrite()写到二进制文件来实现跨平台交换,这会受字节序、对齐、填充等影响。 - 做法:用
memcpy按字段提取,按协议约定字节序(常为大端)用htons/htonl等转换后,再逐字节写出;接收方再做逆转换。
示例(打包为网络大端格式):
#include <stdint.h>
#include <string.h>
#include <arpa/inet.h>
struct Msg {
uint16_t code; // 主机字节序
uint32_t value; // 主机字节序
};
// 输出缓冲区:code(2 bytes, BE) + value(4 bytes, BE)
void serialize_msg(const struct Msg* m, uint8_t out[6]) {
uint16_t be_code = htons(m->code);
uint32_t be_value = htonl(m->value);
memcpy(out + 0, &be_code, sizeof(be_code));
memcpy(out + 2, &be_value, sizeof(be_value));
}
5.2 位域(bit-field)
- 位域的布局和字节序、编译器实现、对齐策略均相关,不可移植。
不建议用位域定义网络协议头;应采用显式移位与掩码。
5.3 强制类型转换与内存别名
- 将
uint32_t*强转为uint8_t*读取字节是可行的(字符类型例外),但其他跨类型别名可能触发未定义行为。
建议通过memcpy做类型安全的拷贝与拆组装。
5.4 文本与二进制
- 文本(例如 JSON、XML、CSV)不受字节序影响;
二进制文件、二进制网络协议需要显式指定字节序。
5.5 浮点数
- 浮点数(
float/double)的二进制布局受 IEEE 754 与字节序影响。
若需跨平台传输浮点,应考虑:- 转为定点数(整数)或文本格式(如十进制字符串);或
- 自己实现固定字节序的二进制打包(谨慎对齐与特殊值)。
6. 自己实现大小端转换(当没有标准函数时)
在非网络场景、或 64 位/自定义大小时可能需要。
16/32/64 位手工转换(以大端为例)
#include <stdint.h>
static inline uint16_t bswap16(uint16_t x) {
return (uint16_t)((x << 8) | (x >> 8));
}
static inline uint32_t bswap32(uint32_t x) {
return ((x & 0x000000FFu) << 24) |
((x & 0x0000FF00u) << 8) |
((x & 0x00FF0000u) >> 8) |
((x & 0xFF000000u) >> 24);
}
static inline uint64_t bswap64(uint64_t x) {
return ((x & 0x00000000000000FFull) << 56) |
((x & 0x000000000000FF00ull) << 40) |
((x & 0x0000000000FF0000ull) << 24) |
((x & 0x00000000FF000000ull) << 8) |
((x & 0x000000FF00000000ull) >> 8) |
((x & 0x0000FF0000000000ull) >> 24) |
((x & 0x00FF000000000000ull) >> 40) |
((x & 0xFF00000000000000ull) >> 56);
}
利用编译器内建优化
- GCC/Clang:
__builtin_bswap16/32/64 - MSVC:
_byteswap_ushort/_byteswap_ulong/_byteswap_uint64
编译器通常会把这些内建函数优化为单条指令(如 BSWAP)。
7. 判断宏与条件编译(若确有需要)
有的平台提供字节序宏(示例,需根据平台具体头文件):
#include <endian.h> // Linux glibc
// 或 #include <machine/endian.h> // BSD系
// 或 #include <sys/param.h> / <sys/endian.h>
#if __BYTE_ORDER == __LITTLE_ENDIAN
// 小端平台
#elif __BYTE_ORDER == __BIG_ENDIAN
// 大端平台
#else
// 未知
#endif
这些宏 非标准 C,可移植性一般。跨平台代码优先使用运行时检测或统一的转换函数。
8. 可视化帮助理解(以 32 位数 0x12345678 为例)
内存地址从低到高:→
- 小端:
[78] [56] [34] [12] - 大端:
[12] [34] [56] [78]
CPU 在取值时会按自身字节序解释这连续的字节序列。
9. 实用建议总结
- 网络通信:始终使用
htons/htonl/ntohs/ntohl。 - 数据交换/文件格式:协议文档必须明确字节序;用
memcpy + 手工位移/bswap实现跨平台一致性。 - 避免位域定义协议;避免直接发送结构体原样二进制。
- 浮点跨平台:优先文本或定点表示;二进制要自己定义固定字节序与编码。
- 写库/接口:在 API 边界尽早规范字节序,保证库内只用主机字节序,I/O 处统一转换。
更多推荐



所有评论(0)