判断大小端

大端与小端是什么

把一个 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。

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 Stack 由 Jimmy 设计