程序跑起来之后,数据在内存里到底是怎么"躺"着的?字节、字长、大小端、基本类型的大小——这一章我们不当读者,当侦探:每一处结论都用本机实测取证
核心方法论:真相只有一个——观察、取证、推理
「案发现场有一具 int 的尸体——0x12345678,被分成了四个字节,丢在四格内存里。问题是:凶手按什么顺序摆放的?顺着放 12 34 56 78,还是倒着放 78 56 34 12?很多程序员一辈子没问过这个问题,直到网络程序在别的机器上莫名跑出乱码。真相只有一个,但取证要靠代码。这一章,跟着我把答案从你的机器里'审'出来。」
上一章把可执行文件加载进内存后,程序开始运行。数据在内存里到底怎么存?先回到最底层:大多数计算机使用 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 位) | 备注 |
|---|---|---|---|
| 0 | 0x00 | 0000 0000 | 一切从零开始 |
| 9 | 0x09 | 0000 1001 | 换行符 \n 的 ASCII 码 |
| 65 | 0x41 | 0100 0001 | 大写字母 A 的 ASCII 码 |
| 127 | 0x7F | 0111 1111 | 有符号 char 的最大值 |
| 255 | 0xFF | 1111 1111 | 无符号 char 的最大值 |
为什么程序员必须会看十六进制?因为调试器、网络抓包、内存 dump 全都用它。学会两件事就够:① 一个字节 = 两个十六进制位;② 0x 开头就是十六进制。看到 0x7F000001 能条件反射出这是 127.0.0.1(回环地址),你的底层功力就入门了。
注意:别把「字节」和「字」混为一谈。字节是 8 位,是内存寻址的基本单位;字(word)是处理器一次能痛快处理的定长数据块,在第 1 章硬件组织里出现过——x86-64 的字长是 8 字节。一句话:字节是"内存的格子",字是"CPU 的饭量"。7.2 节要讲的"字长",指的就是后者。
每台计算机都有一个字长(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 GiB | 2^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 字节——字长和基本类型大小是两回事,别混。
对于跨越多字节的对象(比如 4 字节的 int),必须建立两条规则:① 对象的地址是什么——约定:地址等于所占字节序列中最小的那个,比如 x 的地址是 0x100,它就占 0x100~0x103;② 字节按什么顺序摆放——这就是字节序(byte order)。字节序有两种:小端法(little endian)和大端法(big endian)。说人话:小端 = "小头朝下",低字节(least significant byte,"不重要的那端")放低地址,高字节放高地址;大端 = 按书写顺序放,高字节(most significant byte,"重要的那端")放低地址,和阅读习惯一致。
还是侦探开场那个"尸体":整数 0x12345678 的四个字节是 0x12、0x34、0x56、0x78,0x12 是最高字节,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)在编译时拦住"这台机器和我假设的不一样"的情况。取证代码可以写,生产代码别赌。
什么时候需要知道本机字节序?调试内存 dump、写跨平台序列化、抓网络包对照协议头——三种场景,判断错了全盘皆输。柯南办案从不靠猜,只有一条:取证。判断字节序的本质,是回答一个问题:"多字节数值的最低地址那个字节,到底是低字节还是高字节?" 只要把"最低地址的字节"单独揪出来看一眼,答案自己就跳出来了。两种经典手法都能取证。
方法一:union 共用内存法。联合体(union)的杀手锏:所有成员共用同一块内存的起始地址。让 int a 和 char 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 读内存字节,是底层程序员的职业习惯。
搞定了字节序,再看第二件事:每种基本类型占几个字节。用 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 位 Linux | 64 位 Windows | 备注 |
|---|---|---|---|---|
| char | 1 | 1 | 1 | 标准保证恒为 1 |
| short | 2 | 2 | 2 | 至少 2 |
| int | 4 | 4 | 4 | 至少 2,主流 4 |
| long | 8 | 8 | 4 | LP64 vs LLP64 的坑就在这 |
| long long | 8 | 8 | 8 | 标准保证 ≥ 8 |
| float | 4 | 4 | 4 | IEEE 754 单精度 |
| double | 8 | 8 | 8 | IEEE 754 双精度 |
| 指针(任意 T*) | 8 | 8 | 8 | 等于字长/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。
前几节的知识不落地就只是谈资。现在看一个真实事故现场:小端机器发出的网络包,为什么到对方手里就变味了? 假设你在小端机器(x86、ARM 默认配置)上给对端发端口号 8080(十六进制 0x1F90)。端口号是两个字节,写进协议头后按小端规则排列:低字节在前,先 0x90 后 0x1F,字节流是 90 1F。对端如果是一台大端机器(比如银行的大型机),它按"高字节在前"读,拼出来就是 0x901F = 36943——端口 8080 变成了 36943,连接打到别的服务上。同一个字节流,两种解释,鸡同鸭讲。这就是字节序在网络上引发的真实事故:小端机器发的数据到了大端机器手里,"字里的字节全反了";反之亦然。
解决方案是行业公约:TCP/IP 协议规定,所有多字节字段在网络上统一使用大端,称为"网络字节序"(network byte order,就是大端法)。发送方把本机内部表示(主机字节序)转成网络标准,接收方再转回来。转换由四个标准函数完成:htons、htonl、ntohs、ntohl。名字就是助记: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));
| 函数 | 全称助记 | 转换方向 | 处理对象 |
|---|---|---|---|
htons | host to network short | 主机序 → 网络序 | 16 位数值(端口号等) |
htonl | host to network long | 主机序 → 网络序 | 32 位数值(IPv4 地址等) |
ntohs | network to host short | 网络序 → 主机序 | 16 位数值 |
ntohl | network 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* 转译。真相只有一个,但你得自己动手才能看见。
某台机器上,一个 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 节本机取证看到的真实样子。判断口诀:低地址是低字节 = 小端;低地址是高字节 = 大端。
把 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,两个分支都不命中,程序什么都不输出——这也提醒你:取证的"已知数据"必须是高、低字节一眼可辨的数。
在 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 定宽类型,要么先查数据模型。
不调用任何库函数,自己实现一个 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 在大端机器上被实现为"原样返回"。这也解释了为什么正确姿势永远是调标准函数:标准函数在不同平台上各自正确,你的手写版只有一个平台正确。