深蓝黑背景封面。左侧是左右对比图:顶部标注「0x12345678 存入连续 4 字节,地址从左到右升高」;上方青色行「大端」四个字节格依次是 12、34、56、78,注「高位在前,符合人类书写顺序」;下方紫色行「小端」四个字节格依次是 78、56、34、12,注「低位在前,主流 x86/ARM 默认」;每格下方标注地址 a、a+1、a+2、a+3。对比图下方注「同一台机器上二者必居其一——写跨机器代码前先要问清楚」。右侧大标题「大端小端是什么,怎么判断当前机器的字节序」,副标题「字节在内存里的排队顺序」。左上标注「面试经典 · C/C++」,右上圆角胶囊标注「Day 06 · 11-23」。 图 1:同一个 0x12345678,两种机器上字节排队方向相反。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。字节序是 C/C++ 面试的常驻题,问题本身简单,但它连着网络编程、文件格式、跨平台序列化一整串工程问题,答出这些连线才算答完。

先定义:字节在多字节类型内部怎么排队。

一个 32 位整数 0x12345678 占连续 4 个字节,地址从 a 到 a+3 升高。大端(big-endian):最高有效字节 0x12 放在最低地址 a,然后依次 34、56、78——和人类从左到右的书写顺序一致,「大头在前」。小端(little-endian)正相反:最低有效字节 0x78 放在最低地址,依次 78、56、34、12。两种只是约定,没有优劣;现实的格局是 x86 与主流 ARM 默认小端,而网络协议历史上统一用大端。

注意两个边界:字节序只对多字节类型有意义,单个字节、字符串的逐字节内容、位运算结果都不受影响;它描述的是「字节」的顺序,不是「位」的顺序,别把它和位序搅在一起。

判断当前机器:指针法和 union 法。

思路一样:放一个已知值的多字节变量,去读它最低地址的那个字节。指针法是把 int 的地址强转成 char* 再读首字节;union 法是让一个 uint32 和一个字节数组共享同一块内存,写 uint32、读字节数组。两种写法都合法且常见,union 版本在 C 里是标准认可的类型双关方式之一。

网络传输为什么要统一字节序。

两台字节序不同的机器直接互发二进制整数,小端发出去的 78 56 34 12 会被大端机器读成 0x78563412,数据直接错乱。所以 TCP/IP 协议族规定网络字节序一律是大端,POSIX 提供了一组转换函数:发送前用 htonl/htons(host to network,32 位/16 位)把主机序转成网络序,接收后用 ntohl/ntohs 转回主机序。在大端机器上这几个函数是空操作,在小端机器上做字节翻转——代码不用关心本机是哪种,函数自己会处理。这也是「跨平台代码先问字节序」的出处:凡是要过网络、进文件格式的多字节数据,要么统一走网络序,要么显式逐字节读写。

编译运行,看看本机是哪种。

用 clang 编译运行(clang -Wall -o endian endian.c && ./endian):

#include <stdio.h>
#include <stdint.h>
#include <arpa/inet.h>

int main(void) {
    int x = 1;                       // 小端机器上内存里是 01 00 00 00
    char *p = (char *)&x;
    printf("指针法: 最低地址字节 = 0x%02X -> %s\n",
           (unsigned char)p[0], p[0] == 1 ? "小端" : "大端");

    union { uint32_t u; uint8_t b[4]; } v;
    v.u = 0x12345678;
    printf("union 法: 各地址字节 = %02X %02X %02X %02X -> %s\n",
           v.b[0], v.b[1], v.b[2], v.b[3],
           v.b[0] == 0x78 ? "小端" : "大端");

    uint32_t net = htonl(0x12345678);
    uint8_t *nb = (uint8_t *)&net;
    printf("htonl 后内存字节 = %02X %02X %02X %02X(高位在前)\n",
           nb[0], nb[1], nb[2], nb[3]);
    printf("ntohl 还原 = 0x%08X\n", ntohl(net));
    return 0;
}

本机(Apple Silicon,小端)实测输出:

指针法: 最低地址字节 = 0x01 -> 小端
union 法: 各地址字节 = 78 56 34 12 -> 小端
htonl 后内存字节 = 12 34 56 78(高位在前)
ntohl 还原 = 0x12345678

两种判断方法都给出小端;htonl 之后内存里的字节变成了高位在前的大端排列,ntohl 再原样转回——四个函数的对称性一目了然。

深蓝黑背景概念图。左侧是地址网格对比:一张四行两列的小表,行是地址 a 到 a+3,两列分别给出大端与小端下该地址的字节——大端列从上到下 12、34、56、78,小端列从上到下 78、56、34、12,顶部注「值 = 0x12345678,0x12 是最高有效字节」。右侧是一条自上而下的判断流程链:卡片「int x = 1」向下指向「看最低地址的字节:(char)&x」,再分叉为两个结果卡——青色「读到 01 → 小端(低位先落地)」,紫色「读到 00 → 大端」。其下灰色卡「网络协议统一用大端(网络字节序):发送前 htonl/htons,接收后 ntohl/ntohs」。底部总结条:字节序只影响多字节类型的存储与传输,单字节数据与位运算不受影响;跨平台序列化要么用网络序,要么显式按字节写。 图 2:左边看排列,右边走判断流程,网络序一行收尾。

跨平台代码的稳妥写法。

知道了原理,落地就两条路:过网络的多字节字段统一转网络序收发;写文件格式时要么像 PNG、网络协议那样在规范里钉死字节序,要么干脆逐字节拼装——(b[0]<<24)|(b[1]<<16)|(b[2]<<8)|b[3] 这种位运算写法在任何机器上结果都一样,因为位运算作用于「值」而非「存储」,天然免疫字节序。 protobuf 这类序列化框架底层也正是按显式字节规则编码,才把字节序问题从用户面前彻底拿走。

面试现场把答案连成线。

建议的答题路径:先用 0x12345678 举例讲清两种排列,再给出判断代码(指针法为主,union 法作为备选),然后主动延伸到网络字节序与 htonl 一族函数,最后补一句工程边界——单字节数据和位运算不受字节序影响,结构体直接 memcpy 过网络是事故高发区(字节序之外还叠加对齐问题,两者是两个维度)。能讲到「为什么协议选大端只是历史约定,但约定本身比选哪种更重要」,这题就答完整了。

你在抓包或解析二进制文件时被字节序坑过吗?当时是大端当成了小端,还是反过来?欢迎讲讲事故现场。