第7章:字节序与数据表示:大小端与基本类型

程序跑起来之后,数据在内存里到底是怎么"躺"着的?字节、字长、大小端、基本类型的大小——这一章我们不当读者,当侦探:每一处结论都用本机实测取证

🔍

本章导师:柯南

核心方法论:真相只有一个——观察、取证、推理

「案发现场有一具 int 的尸体——0x12345678,被分成了四个字节,丢在四格内存里。问题是:凶手按什么顺序摆放的?顺着放 12 34 56 78,还是倒着放 78 56 34 12?很多程序员一辈子没问过这个问题,直到网络程序在别的机器上莫名跑出乱码。真相只有一个,但取证要靠代码。这一章,跟着我把答案从你的机器里'审'出来。」

7.1 从字节说起:内存的最小单位与虚拟地址

上一章把可执行文件加载进内存后,程序开始运行。数据在内存里到底怎么存?先回到最底层:大多数计算机使用 8 位的块,也就是字节(byte,8 个二进制位组成的最小存储单元)作为最小的可寻址存储器单位——CPU 能精确读"第 100 号字节",却读不了"第 100 号字节的第 3 位":按位寻址的硬件成本高得离谱,而字符、整数、浮点数天然以字节为倍数。机器级程序把存储器看作一个非常大的字节数组,这个数组就是虚拟存储器(virtual memory,操作系统给程序提供的统一字节数组假象)。每个字节由一个唯一的数字标识,称为它的地址(address);所有可能地址的集合就是虚拟地址空间(virtual address space)。说人话:内存就是一个超大数组,下标就是地址,每个元素恰好一个字节

那地址和字节内容用什么记法?答案是十六进制(hexadecimal,逢 16 进 1,用 0-9 和 A-F 表示 0 到 15)。原因很漂亮:一个字节 = 8 位 = 恰好 2 个十六进制位。二进制写 8 位太长,十进制又跟位对不上,唯独十六进制两两一组严丝合缝。所以调试器、内存 dump、地址打印里满屏都是十六进制:地址写作 0x100,多字节整数写作 0x12345678。快速换算如下:

# 十六进制 ↔ 二进制:一个十六进制位对应 4 个二进制位
0x7F = 0111 1111   // 十进制 127
0x80 = 1000 0000   // 十进制 128,符号位的起点
0xFF = 1111 1111   // 十进制 255,无符号字节最大值
0x12 34 56 78 = 0001 0010 0011 0100 0101 0110 0111 1000  // 4 个字节 = 一个 int

眼见为实:写个小程序,打印 int 变量的地址,再读出最低地址处的字节。注意——地址每次运行都不一样,因为现代操作系统都有 ASLR(地址空间布局随机化,每次启动都把栈和堆的基址打乱,防止黑客利用固定地址攻击)。下面是我在本机(arm64 macOS)连续两次运行的真实输出,地址变了,规律没变:

// addr.cpp —— 打印变量地址,读取最低地址处的字节
#include <cstdio>

int main() {
    int x = 0x12345678;
    unsigned char* p = (unsigned char*)&x;   // 指向 x 占用的第一个字节
    printf("&x   = %p\n", (void*)&x);
    printf("p[0] = 0x%02x (最低地址字节)\n", (int)p[0]);
    printf("p[3] = 0x%02x (最高地址字节)\n", (int)p[3]);
    return 0;
}
# 编译 + 运行(本机 arm64 macOS 真实输出)
g++ addr.cpp -o addr && ./addr
# 第一次:
&x   = 0x16fa5e128
p[0] = 0x78 (最低地址字节)
p[3] = 0x12 (最高地址字节)
# 第二次:地址变为 0x16fa7e128(ASLR 随机化),字节规律不变
十进制十六进制二进制(8 位)备注
00x000000 0000一切从零开始
90x090000 1001换行符 \n 的 ASCII 码
650x410100 0001大写字母 A 的 ASCII 码
1270x7F0111 1111有符号 char 的最大值
2550xFF1111 1111无符号 char 的最大值
柯南提示

为什么程序员必须会看十六进制?因为调试器、网络抓包、内存 dump 全都用它。学会两件事就够:① 一个字节 = 两个十六进制位;② 0x 开头就是十六进制。看到 0x7F000001 能条件反射出这是 127.0.0.1(回环地址),你的底层功力就入门了。

注意:别把「字节」和「字」混为一谈。字节是 8 位,是内存寻址的基本单位;(word)是处理器一次能痛快处理的定长数据块,在第 1 章硬件组织里出现过——x86-64 的字长是 8 字节。一句话:字节是"内存的格子",字是"CPU 的饭量"。7.2 节要讲的"字长",指的就是后者。

7.2 字长:32 位与 64 位系统的差别

每台计算机都有一个字长(word size),指明整数和指针数据的标称大小——指针就是"装地址的变量",指针多大由字长决定。因为虚拟地址就是用字来编码的,字长决定最重要的参数:虚拟地址空间的最大大小。字长为 n 位的机器,虚拟地址范围是 0 ~ 2^n - 1,程序最多访问 2^n 字节。把这条刻进脑子:n 位字长 = n 位地址 = 2 的 n 次方字节的寻址能力

算一算就吓人:32 位机器地址空间 2^32 ≈ 4 GB——当年"内存 4GB 到头"的硬天花板;64 位机器是 2^64 ≈ 16 EB(艾字节,1 EB = 10 亿 GB)。注意这不是翻倍,是平方:地址多一位,空间翻一倍。当然 16 EB 只是理论极限,硬件和操作系统都有限制;但"指针从 4 字节变 8 字节"是实打实的,本机实测如下:

// ptrsize.cpp —— 指针大小 = 字长 / 8
#include <cstdio>

int main() {
    int* p = nullptr;
    printf("sizeof(int*) = %zu 字节\n", sizeof(p));
    return 0;
}
# 本机(64 位 arm64 macOS)真实输出:
g++ ptrsize.cpp -o ptrsize && ./ptrsize
# sizeof(int*) = 8 字节
对比项32 位系统64 位系统
字长 / 指针大小4 字节(32 位)8 字节(64 位)
地址空间上限2^32 ≈ 4 GiB2^64 ≈ 16 EiB
int 装得下地址吗刚好(unsigned int 就是指针大小)装不下,指针要用 long long 或专门类型
指针的内存开销链表、哈希表指针省一半内存同样的结构多花一倍指针内存
如今的状态嵌入式、老设备、兼容模式桌面 / 服务器 / 手机主流
柯南提示

记住一个反直觉的结论:64 位系统不是"两倍的 32 位",而是"地址多 32 位"——空间是 2^64,不是 2^32×2。代价是每个指针从 4 字节涨到 8 字节,全是指针的程序(链表、树、哈希表)内存开销直接变大,这也是 64 位时代很多数据结构要重新设计的原因。

注意"64 位系统"不等于"你的程序是 64 位程序"。在 64 位系统上编译 32 位程序(比如 GCC 加 -m32),它的指针还是 4 字节,照样只能寻址 4GB。另外预告一个 7.5 节的坑:同样是 64 位,Linux/macOS 上 long 是 8 字节,Windows 上 long 却还是 4 字节——字长和基本类型大小是两回事,别混。

7.3 大小端:一个数的两种存放方式

对于跨越多字节的对象(比如 4 字节的 int),必须建立两条规则:① 对象的地址是什么——约定:地址等于所占字节序列中最小的那个,比如 x 的地址是 0x100,它就占 0x100~0x103;② 字节按什么顺序摆放——这就是字节序(byte order)。字节序有两种:小端法(little endian)和大端法(big endian)。说人话:小端 = "小头朝下",低字节(least significant byte,"不重要的那端")放低地址,高字节放高地址;大端 = 按书写顺序放,高字节(most significant byte,"重要的那端")放低地址,和阅读习惯一致。

还是侦探开场那个"尸体":整数 0x12345678 的四个字节是 0x120x340x560x780x12 是最高字节,0x78 是最低字节。假设变量地址是 0x100,两种摆法如下——注意看低地址 0x100 那一格是谁:

# int x = 0x12345678; 地址从 0x100 开始,占 4 个字节
# 地址增大方向:0x100 -> 0x101 -> 0x102 -> 0x103

# 小端法(little endian):低字节放低地址 —— 内存里是"倒着"的
地址:   0x100   0x101   0x102   0x103
内存:  ┌──────┬──────┬──────┬──────┐
       │ 0x78 │ 0x56 │ 0x34 │ 0x12 │   <- 低字节 0x78 在最前面(低地址)
       └──────┴──────┴──────┴──────┘

# 大端法(big endian):高字节放低地址 —— 和书写顺序一致
地址:   0x100   0x101   0x102   0x103
内存:  ┌──────┬──────┬──────┬──────┐
       │ 0x12 │ 0x34 │ 0x56 │ 0x78 │   <- 高字节 0x12 在最前面(低地址)
       └──────┴──────┴──────┴──────┘

顺着地址把字节一个一个打出来,本机的真实答案是:0x78 0x56 0x34 0x12——低字节在前,本机是小端。这跟 7.1 节 p[0]=0x78、p[3]=0x12 的取证结果完全对上:

// byteorder.cpp —— 按地址顺序打印 int 的 4 个字节
#include <cstdio>

int main() {
    int x = 0x12345678;
    unsigned char* p = (unsigned char*)&x;
    for (int i = 0; i < 4; i++)
        printf("addr+%d: 0x%02x\n", i, (int)p[i]);
    return 0;
}
# 本机真实输出:低字节 0x78 在低地址 → 小端法
g++ byteorder.cpp -o byteorder && ./byteorder
addr+0: 0x78   // 低地址 = 低字节
addr+1: 0x56
addr+2: 0x34
addr+3: 0x12   // 高地址 = 高字节
平台 / 处理器字节序说人话
x86 / x86-64(Intel、AMD)小端绝大多数 PC 和服务器,你大概率就站在这里
ARM(手机、Mac、嵌入式)双端,默认小端两种都支持,主流操作系统按小端配置运行
MIPS / RISC-V双端硬件两种都行,看系统怎么配
IBM z/Architecture(大型机)、旧 PowerPC大端金融、银行系统常见,跟书写习惯一致
网络字节序(TCP/IP 协议)大端协议规定的"世界语",7.6 节的主角
柯南提示

大小端只影响多字节的数值类型。单个 char、字符数组、文本,怎么存都是那一串字节,没有大小端问题。还有一条:寄存器里没有大小端——字节序是"内存视图",数据一旦被 CPU 读进寄存器就是完整整数,哪头在前根本不存在。所以大小端只在"内存 ↔ 寄存器搬运"和"跨机器传输"两个环节显形。

注意:永远不要在业务代码里写死字节序假设。"把 int 第 0 个字节当低字节用"这种代码,这台机器上对,换台大端机器就全错。正经做法:跨平台序列化用统一的字节序工具函数(7.6 节),或用编译期断言(static_assert)在编译时拦住"这台机器和我假设的不一样"的情况。取证代码可以写,生产代码别赌。

7.4 侦探取证:如何判断你的机器是哪种

什么时候需要知道本机字节序?调试内存 dump、写跨平台序列化、抓网络包对照协议头——三种场景,判断错了全盘皆输。柯南办案从不靠猜,只有一条:取证。判断字节序的本质,是回答一个问题:"多字节数值的最低地址那个字节,到底是低字节还是高字节?" 只要把"最低地址的字节"单独揪出来看一眼,答案自己就跳出来了。两种经典手法都能取证。

方法一:union 共用内存法。联合体(union)的杀手锏:所有成员共用同一块内存的起始地址。让 int achar c 站在同一块内存上,写入 a = 0x12345678 后读 c,读到的就是 a最低地址字节:c 是 0x78(低字节)就是小端,是 0x12(高字节)就是大端。这方法出自《深入理解计算机系统》的经典习题,也原样保留在咱们的素材笔记里——下面代码照搬原文,仅补了一行 #include <iostream> 才能编译:

// endian_union.cpp —— union 判端序(素材原样,仅补头文件)
#include <iostream>

int main(int argc, char* argv[])
{
    union {
        int a;
        char c;
    }Un;//联合体共用内存
    //假设当前的内存地址为           00 01 02 03
    //int a 4个字节,大端法的内存分布 12 34 56 78;
    //int a 4个字节,小端法的内存分布 78 56 34 12;
    Un.a = 0x12345678;//12是数据的高字节哦
    if (Un.c == 0x12)
        std::cout << "big" << std::endl;
    else if (Un.c == 0x78)
        std::cout << "little" << std::endl;
    return 0;
}
# 编译 + 运行(本机真实输出):
g++ endian_union.cpp -o endian_union && ./endian_union
little

方法二:指针低地址读法。思路一模一样,只是把"读最低地址字节"从 union 换成指针:把 &x 强转成 unsigned char*,解引用就是 x 的第一个字节(对象地址 = 最小地址,7.3 节约定过)。殊途同归,方法二还能把 4 个字节按地址顺序全打出来,证据更完整:

// endian_ptr.cpp —— 指针低地址读法
#include <cstdio>

int main() {
    int x = 0x12345678;
    unsigned char* p = (unsigned char*)&x;   // 指向最低地址字节
    printf("低地址首字节 = 0x%02x\n", (int)p[0]);
    if (p[0] == 0x78)
        printf("little endian (小端)\n");
    else if (p[0] == 0x12)
        printf("big endian (大端)\n");
    return 0;
}
# 编译 + 运行(本机真实输出):
g++ endian_ptr.cpp -o endian_ptr && ./endian_ptr
低地址首字节 = 0x78
little endian (小端)
取证方法原理优点 / 缺点
union 共用内存法union 所有成员共享起始地址,读 char 即读 int 首字节教科书经典、直白;但 union 双读(type punning)是未定义行为,仅适合教学取证
指针低地址读法强转 unsigned char* 后解引用首字节同样直白,还能顺带打印全部字节;对 char 符号性敏感(见下方警告)
柯南提示

取证的三个要素,缺一不可:① 已知数据(0x12345678 这种"高字节 12、低字节 78"一眼可辨的数);② 观察点(最低地址那个字节);③ 观察顺序(从低地址往高地址读)。三要素齐了,大小端无所遁形。侦探和普通人的区别不是看得多,是会看。

注意:读字节一定要用 unsigned char,别用裸 char。多数平台上 char 有符号,首字节若是 0x80 这类高位为 1 的值,会被符号扩展0xffffff80(int 值 -128),一比较就全乱。用 unsigned char 读内存字节,是底层程序员的职业习惯。

7.5 基本类型的存储大小:sizeof 实测

搞定了字节序,再看第二件事:每种基本类型占几个字节。用 sizeof 运算符(编译期求值)一问便知。C++ 标准只给下限保证char 恒为 1 字节;short ≥ 2;int ≥ 2;long ≥ 4;long long ≥ 8;指针大小 = 字长 / 8。具体多少由平台的数据模型(data model,int/long/指针的位宽组合约定)决定:Linux/macOS 的 64 位模型是 LP64(long 和指针都是 64 位),Windows 是 LLP64(只有 long long 和指针是 64 位,long 还是 32 位)——同一个 long,macOS 上 8 字节,Windows 上 4 字节,这是跨平台代码最常见的坑。

纸上谈兵不如实测。下面这个程序把基本类型挨个量了一遍,本机(arm64 macOS,LP64 模型)真实输出如下——注意 long 是 8 字节,Windows 上会差一倍:

// sizeof.cpp —— 本机实测所有基本类型大小
#include <cstdio>

int main() {
    printf("char      = %zu 字节\n", sizeof(char));
    printf("short     = %zu 字节\n", sizeof(short));
    printf("int       = %zu 字节\n", sizeof(int));
    printf("long      = %zu 字节\n", sizeof(long));
    printf("long long = %zu 字节\n", sizeof(long long));
    printf("float     = %zu 字节\n", sizeof(float));
    printf("double    = %zu 字节\n", sizeof(double));
    printf("指针(int*) = %zu 字节\n", sizeof(int*));
    printf("bool      = %zu 字节\n", sizeof(bool));
    printf("size_t    = %zu 字节\n", sizeof(size_t));
    return 0;
}
# 编译 + 运行(本机 arm64 macOS 真实输出):
g++ sizeof.cpp -o sizeof_prog && ./sizeof_prog
char      = 1 字节
short     = 2 字节
int       = 4 字节
long      = 8 字节    // LP64:macOS/Linux 上 long 是 8 字节
long long = 8 字节
float     = 4 字节
double    = 8 字节
指针 (int*) = 8 字节   // 64 位系统指针固定 8 字节
bool      = 1 字节
size_t    = 8 字节
类型本机实测(arm64 macOS)64 位 Linux64 位 Windows备注
char111标准保证恒为 1
short222至少 2
int444至少 2,主流 4
long884LP64 vs LLP64 的坑就在这
long long888标准保证 ≥ 8
float444IEEE 754 单精度
double888IEEE 754 双精度
指针(任意 T*)888等于字长/8
柯南提示

sizeof 最经典的陷阱:数组和指针,长得像,量起来不一样int a[8] 在定义处 sizeof(a) 是 32(整个数组 8×4 字节);可一旦传进函数 void f(int arr[]),形参 arr 退化成指针,函数里 sizeof(arr) 只有 8。一个变量两种答案,原因就是 7.2 节那句话:指针是"装地址的变量",大小只由字长决定。

注意sizeof 的返回值类型是 size_t(无符号整数),打印要用 %zu,千万别用 %d 或塞进 int——大对象或 32 位平台上可能截断。另外别拿 sizeof 当字符串长度:sizeof("hi") 是 3(含结尾的 '\0'),数实际字符得用 strlen

7.6 实战:网络协议里的字节序陷阱

前几节的知识不落地就只是谈资。现在看一个真实事故现场:小端机器发出的网络包,为什么到对方手里就变味了? 假设你在小端机器(x86、ARM 默认配置)上给对端发端口号 8080(十六进制 0x1F90)。端口号是两个字节,写进协议头后按小端规则排列:低字节在前,先 0x90 后 0x1F,字节流是 90 1F。对端如果是一台大端机器(比如银行的大型机),它按"高字节在前"读,拼出来就是 0x901F = 36943——端口 8080 变成了 36943,连接打到别的服务上。同一个字节流,两种解释,鸡同鸭讲。这就是字节序在网络上引发的真实事故:小端机器发的数据到了大端机器手里,"字里的字节全反了";反之亦然。

解决方案是行业公约:TCP/IP 协议规定,所有多字节字段在网络上统一使用大端,称为"网络字节序"(network byte order,就是大端法)。发送方把本机内部表示(主机字节序)转成网络标准,接收方再转回来。转换由四个标准函数完成:htonshtonlntohsntohl。名字就是助记:h = host 主机,n = network 网络,s = short 16 位,l = long 32 位。在小端机器上它们真的倒字节;在大端机器上它们原样返回(网络序就是大端,转了等于没转)——所以代码永远要调用它们,别写"反正我是小端"的假设。下面这段程序在本机实测了四个函数的转换效果:

// htons.cpp —— 主机序与网络序互转实测
#include <cstdio>
#include <cstdint>
#include <arpa/inet.h>   // POSIX 平台提供 htons / htonl / ntohs / ntohl

int main() {
    uint16_t host_port = 8080;                // 端口 8080 = 0x1F90,主机序
    uint16_t net_port  = htons(host_port);     // 主机序 -> 网络序
    printf("host 0x%04x -> net 0x%04x\n", host_port, net_port);

    uint32_t host_ip = 0x7F000001;           // 127.0.0.1,主机序
    uint32_t net_ip  = htonl(host_ip);        // 主机序 -> 网络序
    printf("host 0x%08x -> net 0x%08x\n", host_ip, net_ip);
    // 回程验证:ntoh* 是 hton* 的逆运算
    printf("回程: ntohs(0x%04x)=0x%04x  ntohl(0x%08x)=0x%08x\n",
           net_port, ntohs(net_port), net_ip, ntohl(net_ip));
    return 0;
}
# 编译 + 运行(本机小端 arm64 macOS 真实输出):
g++ htons.cpp -o htons_prog && ./htons_prog
host 0x1f90 -> net 0x901f      // 两个字节颠倒了——小端机器上 htons 真的在倒字节
host 0x7f000001 -> net 0x0100007f  // 4 个字节整体倒序
回程: ntohs(0x901f)=0x1f90  ntohl(0x0100007f)=0x7f000001

用进真实发送流程,规矩就一句话:写进协议头的每个多字节数值先过 hton*,从协议头读出的先过 ntoh*。端口、IP 地址、长度字段,一个都不能漏。标准写法如下(示意代码,省略错误处理):

// 发送 UDP 报文(示意):所有多字节字段先转网络字节序
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

struct sockaddr_in addr;
addr.sin_family      = AF_INET;
addr.sin_port        = htons(8080);        // 端口:主机序 -> 网络序
addr.sin_addr.s_addr = htonl(0x7F000001);  // 127.0.0.1:同样要转

sendto(sock, buf, len, 0,
       (struct sockaddr*)&addr, sizeof(addr));
函数全称助记转换方向处理对象
htonshost to network short主机序 → 网络序16 位数值(端口号等)
htonlhost to network long主机序 → 网络序32 位数值(IPv4 地址等)
ntohsnetwork to host short网络序 → 主机序16 位数值
ntohlnetwork to host long网络序 → 主机序32 位数值
柯南提示

这四个函数的名字本身就是一份完整的案情笔记:h 是 host(主机),n 是 network(网络),s 是 short(16 位),l 是 long(32 位)。方向永远从"当前所在的地方"往"要去的地方"写:要发出去了就 hton(从主机去网络);刚收到了就 ntoh(从网络回主机)。看到 htons 条件反射:发端口号;看到 ntohl:读 IP 地址。

注意:两件事别搞错。① 只有多字节数值需要转——文本、字节数组是逐字节传输的,没有字节序问题,硬转反而坏事。② 别对已转过的值再转一次——htons(htons(x)) 会把字节倒回来(双重转换是经典 bug)。一个值只转一次,方向记清。

这一章的案卷可以归档了:内存是字节数组,地址是下标,字长决定指针大小和地址空间上限;多字节数值分大小端两派,本机是小端,union 或指针低地址读法即可取证;基本类型大小由数据模型决定,long 跨平台不一样;网络协议用大端当"世界语",跨机器传输的数值都交给 hton*/ntoh* 转译。真相只有一个,但你得自己动手才能看见。

章末练习

练习 1:读现场 入门

某台机器上,一个 int 变量 x = 0x12345678 的内存 dump 是(从低地址到高地址):12 34 56 78。请问这台机器是大端还是小端?如果把同样的 x 存在小端机器上,dump 应该长什么样?

提示

看低地址第一个字节是谁:是 0x12(高字节)还是 0x78(低字节)?对照 7.3 节的两张内存布局图。

参考答案

dump 从低地址开始是 12 34 56 78,即高字节 0x12 在低地址——大端法(和书写顺序一致)。同样的 x 在小端机器上是 78 56 34 12,正是 7.4 节本机取证看到的真实样子。判断口诀:低地址是低字节 = 小端;低地址是高字节 = 大端

练习 2:读懂 union 取证 进阶

把 7.4 节那段 union 代码原样放到一台 x86 机器上编译运行,输出是什么?解释为什么。如果把 Un.a = 0x12345678 改成 Un.a = 0x1234,输出会变吗?为什么?

提示

x86 是经典小端;union 的 char 成员读的是 int 的最低地址字节。0x1234 的最低地址字节是多少?

参考答案

x86 小端,最低地址字节是低字节 0x78,输出 little(本机 arm64 macOS 实测也正是 little)。改成 0x1234 后,最低地址字节是 0x34,既不是 0x12 也不是 0x78,两个分支都不命中,程序什么都不输出——这也提醒你:取证的"已知数据"必须是高、低字节一眼可辨的数

练习 3:sizeof 迷案 进阶

在 64 位 macOS 上:int a[8] 在 main 函数里 sizeof(a) 是多少?把 a 传给 void f(int arr[]) 后,f 里 sizeof(arr) 又是多少?如果这份代码被移植到 64 位 Windows 上,sizeof(long) 会从多少变成多少?

提示

数组在函数形参里会退化成指针;回忆 7.5 节的数据模型表格,LP64 和 LLP64 差在哪一个类型?

参考答案

sizeof(a) = 8×4 = 32 字节(整个数组)。② 形参 int arr[] 其实就是 int*,退化成指针,f 里 sizeof(arr) = 8 字节。③ macOS 是 LP64,long = 8 字节;Windows 是 LLP64,long = 4 字节,long long 才是 8 字节。同一个 long 跨平台差一倍——跨平台代码要么用 int32_t/int64_t 定宽类型,要么先查数据模型。

练习 4:手写 htonl 挑战

不调用任何库函数,自己实现一个 uint32_t my_htonl(uint32_t v),把小端机器上的 32 位数值转成网络字节序(大端)。提示:把 4 个字节分别取出来,按相反的顺序重新拼装。然后回答:这个函数在大端机器上运行,结果应该是什么?

提示

取字节用移位和掩码:v & 0xFF 拿最低字节,v >> 8 挪到最低位。拼装时把"最低字节"放到最高位。

参考答案

实现:return ((v & 0xFF) << 24) | ((v & 0xFF00) << 8) | ((v >> 8) & 0xFF00) | ((v >> 24) & 0xFF);——最低字节挪到最高位,次低挪到次高,依次倒序拼装。验证:v = 0x7F000001 时返回 0x0100007F,和 7.6 节 htonl 的实测输出一致。在大端机器上,内存本来就按大端排,my_htonl 会把字节再倒一遍,结果反而错了——所以标准库的 htonl 在大端机器上被实现为"原样返回"。这也解释了为什么正确姿势永远是调标准函数:标准函数在不同平台上各自正确,你的手写版只有一个平台正确