大端与小端是什么
把一个 int(4 字节)写进内存,问题就来了:多字节数据里,到底哪个字节放在低地址?
- 大端(Big-Endian):高位字节放在低地址,和我们书写数字的习惯一致
- 小端(Little-Endian):低位字节放在低地址,读写起来"反直觉",但硬件实现更自然
以 0x01020304 存入起始地址 0x1000 为例:
1
2
3
4
5
|
低地址 ──────────────────► 高地址
0x1000 0x1001 0x1002 0x1003
大端 01 02 03 04 高位字节 01 在低地址
小端 04 03 02 01 低位字节 04 在低地址
|
两种存储方式本身没有优劣之分,真正的麻烦出现在跨平台传输数据时:大端机器写出去的字节流,小端机器按同样的偏移读出来,得到的就是 0x04030201——完全错了。网络字节序(Network Byte Order)规定为大端,正是为了消除这种歧义,htonl() / ntohl() 这对函数就是干这个的。
联合体法
C 语言标准保证联合体(union)的所有成员共享同一块内存,起始地址相同。于是可以让 int 和 char[4] 重叠,直接看那个字节:
1
2
3
4
5
6
7
8
9
10
11
12
|
int check_endian(void)
{
union {
int i; /* 4 字节整数 */
char c[4]; /* 逐字节访问 */
} u;
u.i = 0x01020304;
/* 小端: c[0]==0x04;大端: c[0]==0x01 */
return (u.c[0] == 0x04) ? 1 : 0; /* 1 = 小端, 0 = 大端 */
}
|
其内存布局:
1
2
3
4
5
6
7
8
9
|
低地址 ──────────────────► 高地址
union ┌────────┬────────┬────────┬────────┐
成员 i: │ 0x01 │ 0x02 │ 0x03 │ 0x04 │ ← 按 int 视角的值(示意)
├────────┼────────┼────────┼────────┤
成员 c: │ c[0] │ c[1] │ c[2] │ c[3] │ ← 按 char 视角的字节
└────────┴────────┴────────┴────────┘
▲
这一个字节在小端机上是 0x04,大端机上是 0x01
|
注意上图中 0x01…0x04 是"数值视角"的示意——具体哪个字节落在哪个格子,正是由端序决定的:小端机会把 0x04 填进 c[0],大端机则填 0x01。
指针强转法
取一个整数的地址,强转成 char * 再解引用,效果与联合体完全相同,代码还更短:
1
2
3
4
5
6
7
|
int check_endian(void)
{
int i = 0x01020304;
char *p = (char *)&i;
return (p[0] == 0x04) ? 1 : 0; /* 1 = 小端, 0 = 大端 */
}
|
原理是同一个:&i 指向这个 int 的最低地址,而 char 类型恰好能"看见"一个字节。无论 i 占 4 字节还是 8 字节,p[0] 永远是地址最小的那个字节。
本质是同一件事:构造一个数值已知的整数,然后查看它在内存最低地址处的那个字节。
GCC 内建函数
编译器早就内置了这个判断,无需自己实现:
1
2
3
4
5
6
7
8
9
10
|
#include <stdio.h>
int main(void)
{
if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
printf("little endian\n");
else
printf("big endian\n");
return 0;
}
|
这是编译期常量,配合 __BYTE_ORDER__、__ORDER_BIG_ENDIAN__、__ORDER_LITTLE_ENDIAN__ 三个宏即可。GCC、Clang 都支持,Linux 内核里也有类似的 __BYTE_ORDER 判断。
实测验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#include <stdio.h>
#include <stdint.h>
int main(void)
{
uint32_t x = 0x01020304;
unsigned char *p = (unsigned char *)&x;
printf("内存字节序: %02x %02x %02x %02x\n", p[0], p[1], p[2], p[3]);
if (p[0] == 0x04)
printf("小端 (Little-Endian)\n");
else if (p[0] == 0x01)
printf("大端 (Big-Endian)\n");
else
printf("未知端序\n");
return 0;
}
|
在常见的 x86 / x86_64 / ARM(默认)机器上输出:
1
2
|
内存字节序: 04 03 02 01
小端 (Little-Endian)
|
若在 MIPS(大端配置)、某些 PowerPC 或网络处理器上运行,输出则是:
1
2
|
内存字节序: 01 02 03 04
大端 (Big-Endian)
|
实际场景:网络编程中的字节序转换
网络编程里字节序几乎无处不在,理解端序后这几行代码就不必死记:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include <arpa/inet.h>
#include <stdio.h>
int main(void)
{
uint32_t host = 0x01020304;
uint32_t net = htonl(host); /* host to network long */
printf("网络序: 0x%08x\n", net);
/* 抓包或跨平台传输时,永远以网络序(大端)为准 */
return 0;
}
|
htonl() / htons():本机序 → 网络序(大端)
ntohl() / ntohs():网络序 → 本机序
在小端机上这些函数会做字节翻转,在大端机上则什么都不做。这也是为什么协议文档里总写"以网络字节序传输"——它把端序问题统一推给了操作系统提供的转换接口,应用层只需保证调用一次 htons/htonl、读取时调用一次 ntohs/ntohl。