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 字节序影响什么?

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

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

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 00un.c[0] 读到的是 01 → 小端。

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

💡 union 为什么有效? :联合体的大小等于最大成员的大小,所有成员从同一个起始地址 开始存放。把 intchar[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 版本):把 intchar[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
相关推荐
黑泽明*1 小时前
LVS-Keepalived-全解析
服务器·开发语言·笔记·学习·php
动词ing1 小时前
【C语言】命名规范
c语言·开发语言
大可-1 小时前
Go Air 热重载安装配置指南
开发语言·后端·golang
Dovis(誓平步青云)1 小时前
《 固井工程软件 Cemsol 的数据管理与国产化适配实践》
android·java·开发语言·人工智能
看浪的路人1 小时前
第6讲:SQL 解析器
java·开发语言·数据库
小则又沐风a2 小时前
负载均衡式在线OJ---------第二幕
linux·c++·后端
a3535413822 小时前
C++项目如何架构优化
java·c++·架构
键盘会跳舞2 小时前
C++:智能指针源码级深度拆解——从 RAII 思想到引用计数的内存安全体系
c++·智能指针·unique_ptr·shared_ptr·weak_ptr
breeze jiang2 小时前
Next.js App Router 全栈实战:从 SPA 的 SEO 痛点到服务端组件与 Hydration 水合机制
开发语言·javascript·ecmascript