C语言大小端字节序:内存里字节的排列顺序

适用对象:学过数据类型在内存中存放的同学

本章目标:彻底搞懂什么是大小端 + 如何用代码检测 + 为什么网络协议必须统一字节序


引入:为什么整数 0x1234567 在内存里不是顺序排放的?

想象一下,你买了一套乐高积木,包装盒上写着「适合 8 岁以上」。你打开一看,里面有 7 个零件:小、中、大、特大、超大、超超大、超超大-Plus(以此类推)。

现在让你把这套乐高装进一个只能放 7 个格子的收纳盒。

方案 A :从左到右按「小→中→大→特大......」的顺序放

方案 B:从左到右按「超超大-Plus→......→中→小」的顺序放

这两种方案,都是把同样的 7 个零件装进同样的盒子,但顺序完全相反。

这就是「大小端」(Endian)的由来。


第一部分:大小端的定义

1.1 概念引入

假设有一个十六进制整数 0x12 34 56 78(四个字节),它要存进内存。

大端模式(Big Endian):

把高位字节放在低地址 ,低位字节放在高地址 。

换句话说:大的放在前面(低地址端),所以叫 "Big Endian"。

内存从左到右(低地址 → 高地址):

复制代码
地址:    0x00    0x01    0x02    0x03
数据:    0x12    0x34    0x56    0x78

小端模式(Little Endian):

把低位字节放在低地址 ,高位字节放在高地址 。

换句话说:小的放在前面(低地址端),所以叫 "Little Endian"。

内存从左到右(低地址 → 高地址):

复制代码
地址:    0x00    0x01    0x02    0x03
数据:    0x78    0x56    0x34    0x12

💡 记忆口诀 :「大 端 = 大 字在小 地址」,「小 端 = 小 字在小地址」。

1.2 生活中的比喻

想象你写一本书,作者简介页写着「张三,生于 1998 年」。

  • 大端:按自然阅读顺序写:1998 → 19 → 1(从年份高位写到低位)
  • 小端:从后往前写:8 → 98 → 1998(从日期低位写到高位)

两种写法都记录了同样的信息「1998 年」,但写的顺序完全相反。

1.3 为什么叫 "Endinn"?

"Endian" 这个词来自乔纳森·斯威夫特的《格列佛游记》:

小人国里两个党派因为吃鸡蛋应该从大端(big end) 还是小端(little end) 敲开而争论不休。

这个典故生动地说明了:两种方式记录的是同样的信息,但顺序完全不同。


第二部分:为什么会有大小端?

2.1 历史原因

大小端的分歧来自计算机硬件设计早期的决策。不同的 CPU 厂商选择了不同的内存存储方式:

CPU 类型 字节序
x86 / x64(Intel、AMD) 小端
大多数 ARM 处理器 小端(可配置为大端)
网络协议(TCP/IP) 大端(也叫「网络字节序」)
MIPS(部分) 大端
RISC-V 小端(标准规定)

2.2 字节序影响什么?

单字节的数据不受影响 :因为只有一个字节,不存在「排列顺序」的问题。char、unsigned char 等永远是一个字节,存和取都一样。

多字节数据才受影响 :所有大于 1 个字节的类型(int、short、long、float、double)在内存中的存储方式,都和字节序有关。

c 复制代码
int a = 0x12345678;  // 4 个字节

// 小端机器:0x78 在低地址,0x12 在高地址
// 大端机器:0x12 在低地址,0x78 在高地址

2.3 为什么网络协议要用大端?

早期的网络设备(来自不同厂商)需要统一通信标准。如果各用各的字节序,就像两个人用不同的语言说数字------

「一百二十三」vs「三二一」

所以 TCP/IP 协议栈(Socket 编程)统一规定:使用大端字节序传输数据 。这叫「网络字节序(Network Byte Order)」。

在发送数据之前,要把本地字节序(大端/小端)转换成网络字节序(大端);收到数据后,再转换回本地字节序。


第三部分:如何用代码检测大小端?

3.1 方法一:指针类型转换

c 复制代码
#include <stdio.h>

int check_endian()
{
    unsigned int x = 0x1;         // 0x00 00 00 01
    unsigned char *p = (unsigned char*)&x;  // 强转为单字节指针

    if (*p == 1)    // 看最低地址的那个字节是什么
        return 1;   // 返回 1 → 小端(0x01 在低地址)
    else
        return 0;   // 返回 0 → 大端(0x00 在低地址)
}

int main()
{
    if (check_endian())
        printf("小端模式(Little Endian)\n");
    else
        printf("大端模式(Big Endian)\n");

    return 0;
}

推演(小端机器) :x = 0x00 00 00 01,低地址字节 = 0x01,*p == 1 成立 → 小端。

推演(大端机器) :x = 0x00 00 00 01,低地址字节 = 0x00,*p != 1 → 大端。

3.2 方法二:联合体(union)共用内存

联合体的所有成员共享同一块内存,而且起始地址相同。用这个特性可以巧妙检测字节序:

c 复制代码
#include <stdio.h>

union Un {
    int a;        // 4 字节
    char c[4];    // 同样 4 字节,和 a 共用同一块内存
};

int main()
{
    union Un un;
    un.a = 0x00000001;   // 最低位字节为 1

    if (un.c[0] == 1)
        printf("小端\n");
    else
        printf("大端\n");

    return 0;
}

推演(小端机器) :un.a 的内存布局 = 01 00 00 00,un.c[0] 读到的是 01 → 小端。

推演(大端机器) :un.a 的内存布局 = 00 00 00 01,un.c[0] 读到的是 00 → 大端。

💡 union 为什么有效? :联合体的大小等于最大成员的大小,所有成员从同一个起始地址 开始存放。把 int 和 char[4] 放进同一个 union,就相当于从 int 的角度和从 char[4] 的角度同时观察同一块内存,字节序一目了然。


第四部分:大小端的实战影响

4.1 文件格式与字节序

某些文件格式(如 BMP 图像、MP3)的头部字段有固定的字节序要求。如果字节序不对,图片打不开、音频播不了。

c 复制代码
// BMP 文件头的前 2 字节是 "BM"(0x42 0x4D)
// 不受字节序影响(单字节),永远是 0x42 0x4D

// 但 BMP 的像素数据宽度字段(4 字节整数)就受字节序影响
// 小端机器写的是 0x00 00 04 00(十进制 1024)
// 大端机器写的是 0x00 00 04 00

4.2 网络通信中的字节序转换

POSIX 标准提供了四个转换函数(在 <arpa/inet.h> 或 <ws2tcpip.h> 中):

c 复制代码
#include <arpa/inet.h>

// 本地字节序 → 网络字节序(大端)
uint32_t htonl(uint32_t hostlong);   // host to network, long (32位)
uint16_t htons(uint16_t hostshort);  // host to network, short (16位)

// 网络字节序 → 本地字节序
uint32_t ntohl(uint32_t netlong);    // network to host, long
uint16_t ntohs(uint16_t netshort);   // network to host, short

🔑 记忆口诀 :h = host(本地),n = network(网络),to 在中间,数字在最后(短变短、长变长)。

典型用法:发送结构化的网络数据包

c 复制代码
#include <stdio.h>
#include <arpa/inet.h>

int main()
{
    // 发送端(假设是小端机器)
    unsigned short port = 8080;
    unsigned int ip = 0xC0A80101;  // 192.168.1.1

    unsigned short net_port = htons(port);  // 转网络字节序
    unsigned int   net_ip   = htonl(ip);   // 转网络字节序

    printf("本地 port: %u, 网络 port: %u\n", port, net_port);
    printf("本地 IP:   0x%x, 网络 IP: 0x%x\n", ip, net_ip);

    return 0;
}

⚠️ 提醒 :如果你在 Windows 上写 Socket 程序,<arpa/inet.h> 可能找不到,换 <winsock2.h> 或 <ws2tcpip.h>。Linux / macOS 上用 POSIX 接口即可。

4.3 不同字节序机器之间传输数据会怎样?

假设小端机器 A 向大端机器 B 发送整数 0x12345678(4 字节原始数据)。

  • A 发送的字节流:78 56 34 12(按内存顺序)
  • B 收到后按大端解析:0x78 56 34 12 = 十进制 2018915336(完全错误的值)

这就是为什么字节序转换是不可省略的。


第五部分:经典面试题精讲

面试题一:什么是大小端?如何用代码判断?

参考答案:

大端模式:高位字节存放在低地址,低位字节存放在高地址。

小端模式:低位字节存放在低地址,高位字节存放在高地址。

判断方法(union 版本):把 int 和 char[4] 放进 union,给 int 赋值为 0x00000001,看 char[0] 的值。如果等于 1,说明最低地址那个字节是 1,就是小端;如果等于 0,就是大端。


面试题二:为什么 x86 是小端,但网络协议用大端?

参考答案:

这是历史和实用性的权衡:

  • x86 采用小端,是因为 CPU 设计时认为低位字节先处理更高效(从低地址开始取指/取数,硬件实现更简单)
  • 网络协议用大端,是因为 TCP/IP 协议诞生于 70 年代(大端机器先出现),加上大端在某些场景下打印数值更直观(高位在前,符合人类阅读习惯)

关键结论 :大小端只是硬件设计选择,没有绝对的好坏;但网络通信必须统一字节序,所以有了 htonl / ntohl 这一套转换函数。


面试题三:下面这段代码在大小端机器上分别输出什么?

c 复制代码
#include <stdio.h>

int main()
{
    int a = 0x1234;
    char *p = (char*)&a;

    printf("%d\n", *p);    // p 指向最低地址那个字节
    return 0;
}

推演:

  • 小端机器 :a = 0x1234 在内存中 = 34 12 00 00(低地址在前),*p = 0x34 = 52
  • 大端机器 :a = 0x1234 在内存中 = 00 00 12 34,*p = 0x00 = 0

答案:小端机器输出 52,大端机器输出 0。


本章总结

大小端对比表:

大端(Big Endian) 小端(Little Endian)
定义 高位字节在低地址 低位字节在低地址
内存布局(0x12345678) 12 34 56 78 78 56 34 12
x86/x64 平台 ❌ ✅(Intel/AMD)
网络字节序 ✅(TCP/IP 标准) ❌
人类阅读习惯 ✅(高位在前更直观) ❌

检测大小端的两种方法:

方法 原理 代码核心
指针强转 把 int 指针强转为 char*,读低地址字节 *(char*)&x == 1 → 小端
union 共用体 int 和 char4 共用同一块内存 un.c[0] == 1 → 小端

字节序转换函数:

函数 含义 场景
htons host → network short (16位) 端口号
htonl host → network long (32位) IP 地址
ntohs network → host short 接收端口号
ntohl network → host long 接收 IP
相关推荐
有Li1 小时前
EEG脑电图数据处理基础
人工智能·算法·机器学习·语言模型·医学生
霸道流氓气质1 小时前
AgentFlow 入门与实战:Task-Based 并发、EWMA 动态调度与四道门禁的 Java 生产级编排
java·开发语言
XMXFS1 小时前
小红书推荐算法底层逻辑解析:用数据思维做内容优化
python·算法·新媒体运营·推荐算法
0+1111 小时前
算法 --栈
算法·leetcode
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 2 章 几何内核
算法·开源
Zwawa2 小时前
PID 的三个参数之外:位置式、增量式和反馈控制
c语言·单片机·嵌入式
鶴哥只手遮天2 小时前
Gmsh 源码深度解析-第 9 章 后处理与插件体系
算法·开源
鶴哥只手遮天2 小时前
Gmsh 源码深度解析-第 7 章 网格质量、优化与高级技术
算法·开源
鶴哥只手遮天2 小时前
OpenFOAM 源码深度剖析 · 第二季-15 实战:手写一个求解器、库与边界条件
算法·开源
鶴哥只手遮天2 小时前
Gmsh 源码深度解析-第 8 章 数值内核与高阶有限元
算法·开源