跳转到内容

Lesson 12: 判断大小端 CNB

练习任务

编写一个 C 程序,用 union(联合体)判断当前机器的字节序(字节顺序/Endianness)。设置 u.i = 1,然后检查其第一个字节 u.c[0] 是否等于 1。如果是则为小端(Little-Endian),return 1;否则为大端(Big-Endian),return 0。大多数 PC 和 ARM 设备都是小端。

提示union 的妙处在于所有成员共享同一段内存——修改 u.i 会反映在 u.c[0] 上。如果 u.i = 1,在小端机器上 u.c[0] == 1(低字节在前),在大端机器上 u.c[0] == 0(低字节在后)。sizeof(union) 等于其最大成员的大小——在这儿是 4intchar[4] 都是 4 字节)。

核心知识点

  • union 联合体 — 所有成员从同一内存地址开始,共享存储空间,同一时刻只有一个成员有效
  • sizeof(union) — 联合体的大小等于最大成员的大小(而非所有成员之和)
  • 类型双关(Type Punning) — 通过 union 用多种类型合法解读同一块内存(C 标准允许,区别于指针强转)
  • 小端字节序(Little-Endian) — 低字节存储在低地址,高字节存储在高地址(x86、ARM 默认)
  • 大端字节序(Big-Endian) — 高字节存储在低地址,低字节存储在高地址(网络字节序、PowerPC)
  • union 检测字节序 — 写 int 值,读 char[0] 首字节,利用共享内存特性判断
  • 指针强转法检测字节序 — *(unsigned char*)&i 直接以单字节视角解读 int 的首地址
  • 位运算法检测字节序 — 不做类型转换,用 (i >> 0) & 0xFF 提取字节后比较
  • 网络字节序与 htonl/ntohl/htons/ntohs — TCP/IP 规定大端,发送前必须统一转换
  • 函数式宏 #define — 含参数的宏,#expr 字符串化,## 记号拼接
  • 多行宏的 do { ... } while(0) 惯用法 — 让多语句宏在任何上下文中都安全
  • IEEE 754 浮点格式 — 1.0f = 0x3F800000,以及 NaN、Inf、非规约数等特殊值
  • 结构体对齐与 #pragma pack — 控制内存对齐,确保 union 成员边界一致

代码框架

endian.c
c
union endian_test
{
    unsigned char c[4];   // 4 字节的字符数组视角
    int i;                // 同一块内存的整数视角
};

int main(void)
{
    union endian_test u;

    // 第一步: 将 u.i 设置为 1
    //   如果机器是小端: 内存字节为 01 00 00 00
    //   如果机器是大端: 内存字节为 00 00 00 01

    // 第二步: 检查 u.c[0](这块内存的第一个字节)
    //   如果是 1 → return 1 → 低字节在低地址 → Little-Endian
    //   如果是 0 → return 0 → 低字节在高地址 → Big-Endian

    if (/* u.c[0] 等于 1 吗? */)
        return 1;    // 小端
    else
        return 0;    // 大端
}

填充框架的关键思考:为什么写入 u.i = 1 之后可以去读 u.c[0]sizeof(union endian_test) 是多少——4 还是 8?如果改成 shortchar[2],逻辑是否依然成立?

TIP

先不要往下翻看参考解答。你可以在代码里先用 printf 打印 u.c[0] ~ u.c[3] 的值来观察完整字节排列,直观体会「共享内存」的含义,验证后再改成 return 语句。


深度讲解

1. union 联合体:所有成员共享一段内存

1.1 从 struct 到 union:内存布局的颠覆

回顾 Lesson 11 的结构体 struct——每个成员各自占有独立的内存空间,按声明顺序依次排列:

struct_layout.c
c
#include <stdio.h>

struct s {
    int i;     // 偏移 0,占 4 字节
    char c;    // 偏移 4,占 1 字节 → 3 字节对齐填充
};

int main(void)
{
    struct s x;
    printf("sizeof(struct s) = %zu\n", sizeof(x));       // 8
    printf("offset of i = %zu\n", (char*)&x.i - (char*)&x);  // 0
    printf("offset of c = %zu\n", (char*)&x.c - (char*)&x);  // 4
    return 0;
}

union 恰好相反——所有成员从同一个起始地址开始,共享同一段内存区域:

union_layout.c
c
#include <stdio.h>

union u {
    int i;        // 偏移 0(与 c[0] 完全重叠)
    char c[4];    // 偏移 0(与 i 完全重叠)
};

int main(void)
{
    union u x;
    printf("sizeof(union u) = %zu\n", sizeof(x));         // 4
    printf("address of x.i   = %p\n", (void*)&x.i);
    printf("address of x.c   = %p\n", (void*)&x.c);       // 相同!
    printf("address of x.c[0]= %p\n", (void*)&x.c[0]);    // 相同!
    return 0;
}

内存布局对照(核心图解):

struct s 的内存布局 (8 字节):             union u 的内存布局 (4 字节):
┌──────────┬────┬───────────┐              ┌──────────┐
    i c 填充(pad)    i
 (4 字节) │(1) │   (3)     │              │ (4 字节) │
├──────────┼────┼───────────┤              ├──────────┤
 偏移0    │偏移4│ 偏移5   c[0] i 最低字节完全重叠
└──────────┴────┴───────────┘   c[1]
   c[2]
各成员有独立地址,互不重叠   c[3]
                                           └──────────┘
                                           偏移0

                                           所有成员从同一地址开始
                                           &x.i == &x.c == &x.c[0]

IMPORTANT

指针 &x.i&x.c&x.c[0] 在 union 中指向同一个地址。这就是 union 能实现「写 int,读 char[0]」的根本原因——它们共享内存的起始位置。

1.2 sizeof(union) = 最大成员的大小

sizeof_union.c
c
#include <stdio.h>

union endian_test {
    unsigned char c[4];   // 4 字节
    int i;                // 4 字节
};

union mixed {
    char c;               // 1 字节
    int i;                // 4 字节
    double d;             // 8 字节  ← 决定了 union 的大小
};

int main(void)
{
    printf("sizeof(union endian_test) = %zu\n",
           sizeof(union endian_test));                    // 4
    printf("sizeof(union mixed)      = %zu\n",
           sizeof(union mixed));                          // 8
    printf("sizeof(double)           = %zu\n",
           sizeof(double));                               // 8
    return 0;
}

原理:union 只需拥有足够容纳其最大成员的空间——因为任何时刻只有一个成员被「有意义地」使用。如果在 mixed 中存入 char,其余 7 字节不被它利用;存入 double,则恰好占满整个空间。

WARNING

union 自身不记录「当前激活的是哪个成员」——这是程序员的责任。如果你写入了 u.i 然后读取 u.d,结果在 C 标准中是未定义行为(虽然大多数编译器会直接将 int 的位模式重新解释为 double 的位模式)。

1.3 类型双关(Type Punning):C 标准允许的合法用法

type_punning.c
c
#include <stdio.h>

union u {
    int i;
    unsigned char c[4];
} x;

int main(void)
{
    x.i = 0x12345678;

    // 现在可以「以 char 数组的视角」查看同一个 int 的字节排列:
    printf("0x%02x 0x%02x 0x%02x 0x%02x\n",
           x.c[0], x.c[1], x.c[2], x.c[3]);
    // 小端输出: 0x78 0x56 0x34 0x12
    // 大端输出: 0x12 0x34 0x56 0x78

    // 反过来:写入 char 数组,用 int 视角读出
    x.c[0] = 0x41; x.c[1] = 0x42; x.c[2] = 0x43; x.c[3] = 0x44;
    printf("0x%08x\n", x.i);
    // 小端输出: 0x44434241(字节顺序颠倒)
    // 大端输出: 0x41424344(保持原顺序)

    return 0;
}

这就是 Type Punning(类型双关)——在不复制数据的前提下,用多种类型解读同一块内存的核心机制。ISO C 标准明确允许通过 union 做类型双关(C99 §6.5.2.3 注脚 82),是比指针强转更标准的做法

NOTE

通过指针强转做类型双关(如 *(float*)&i)在 C 的「严格别名规则」(strict aliasing)下是未定义行为——编译器优化时可能产生意外结果。本课末尾将详细讨论这一点。

1.4 深入理解:写入后的读取演示

为了加深对 union 共享内存机制的理解,以下程序逐步展示每次写入一个成员后所有成员的值变化:

union_step_by_step.c
c
#include <stdio.h>

union demo {
    unsigned char c[4];
    int i;
};

void print_state(union demo *p)
{
    printf("  hex: 0x%02x 0x%02x 0x%02x 0x%02x  |  as int: %d (0x%08x)\n",
           p->c[0], p->c[1], p->c[2], p->c[3],
           p->i, (unsigned int)p->i);
}

int main(void)
{
    union demo u;

    // 初始状态(栈上的垃圾值)
    printf("Initial (garbage):\n");
    print_state(&u);

    // 写入 1
    u.i = 1;
    printf("After u.i = 1:\n");
    print_state(&u);
    // 小端: 0x01 0x00 0x00 0x00  |  as int: 1
    // 大端: 0x00 0x00 0x00 0x01  |  as int: 1

    // 写入 0x12345678
    u.i = 0x12345678;
    printf("After u.i = 0x12345678:\n");
    print_state(&u);
    // 小端: 0x78 0x56 0x34 0x12  |  as int: 305419896
    // 大端: 0x12 0x34 0x56 0x78  |  as int: 305419896

    // 通过 char 数组逐个修改
    u.c[0] = 0xAA;
    u.c[3] = 0xBB;
    printf("After modifying u.c[0] and u.c[3]:\n");
    print_state(&u);
    // u.i 的值改变了——因为 c[0] 和 c[3] 与 i 的某些字节重叠

    return 0;
}

运行这个程序可以直观地看到「写入一个成员,其他成员的值随之变化」——这是理解 union 本质的最佳方式。


2. 字节序:数字在内存中的存放方式

2.1 问题:0x12345678 怎么存储?

考虑一个 32 位整数 0x12345678,它由 4 个字节组成:

32 位整数: 0x12345678
┌──────┬──────┬──────┬──────┐
 0x12 0x34 0x56 0x78 从最高有效字节到最低有效字节
 MSB LSB
└──────┴──────┴──────┴──────┘

MSB = Most Significant Byte(最高有效字节)
LSB = Least Significant Byte(最低有效字节)

在内存中存储这 4 个字节时,有两种排列方式——两种都是正确的,不同 CPU 架构有不同的选择:

小端 (Little-Endian): 低字节存放在低地址
┌──────┬──────┬──────┬──────┐
 0x78 0x56 0x34 0x12 LSB first(最低有效字节在前)
└──────┴──────┴──────┴──────┘
 低地址   ───────────→    高地址

大端 (Big-Endian): 高字节存放在低地址
┌──────┬──────┬──────┬──────┐
 0x12 0x34 0x56 0x78 MSB first(最高有效字节在前)
└──────┴──────┴──────┴──────┘
 低地址   ───────────→    高地址

记忆口诀

  • 小端(Little-Endian)的(低位的)字节放在的地址——"Little end comes first"
  • 大端(Big-Endian)的(高位的)字节放在的地址——"Big end comes first"

2.2 名字的由来:小人国的鸡蛋战争

这两个词源自 1980 年 Danny Cohen 的著名技术备忘录《On Holy Wars and a Plea for Peace》。文中引用了乔纳森·斯威夫特的小说《格列佛游记》:

小人国(Lilliput)中两个派别因吃鸡蛋应该从哪一端打破而分裂——一派从大端(big end),另一派从小端(little end)。这场争执导致多年的战争。Cohen 借用这个典故来形容计算机行业中关于字节排列方式的「阵营分歧」——虽然不会像小说里那样兵戎相见,但也确实导致了大量互操作性问题。

2.3 各平台字节序、多字节类型一览

平台字节序说明
x86 / x86-64(Intel / AMD)小端PC 和服务器的主流架构
ARM(默认模式)小端手机和嵌入式的主流,但有芯片支持大端切换
RISC-V(默认)小端新兴开源指令集
MIPS(可配置)均可启动时可通过引脚配置字节序
PowerPC(部分)大端老款 Mac(PowerPC 时代)、部分嵌入式
SPARC(传统)大端Oracle/Sun 服务器
网络字节序大端TCP/IP 协议规定的统一格式

不同整数类型的字节序表现

0x12345678 为例,不同宽度的类型在小端机器上的内存布局如下:

类型      十六进制值          内存布局 (低→高地址)
int32_t   0x12345678         [78][56][34][12]
int16_t   0x1234             [34][12]  (只取低 16)
int8_t    0x12              [12]  (只有一字节,无字节序问题)

对于 short(16 位),字节序依然存在——0x1234 在小端上是 [34][12],在大端上是 [12][34]

2.4 用程序验证不同整数的字节序

endian_demo.c
c
#include <stdio.h>

typedef union {
    unsigned short s;
    unsigned char  c[2];
} short_union;

typedef union {
    unsigned int   i;
    unsigned char  c[4];
} int_union;

int main(void)
{
    short_union su;
    int_union   iu;

    su.s = 0x1234;
    iu.i = 0x12345678;

    printf("=== 16-bit short 0x1234 ===\n");
    printf("  Byte 0: 0x%02x\n", su.c[0]);
    printf("  Byte 1: 0x%02x\n", su.c[1]);

    printf("=== 32-bit int 0x12345678 ===\n");
    printf("  Byte 0: 0x%02x (LSB)\n", iu.c[0]);
    printf("  Byte 1: 0x%02x\n", iu.c[1]);
    printf("  Byte 2: 0x%02x\n", iu.c[2]);
    printf("  Byte 3: 0x%02x (MSB)\n", iu.c[3]);

    if (iu.c[0] == 0x78)
        printf("\nResult: Little-Endian\n");
    else
        printf("\nResult: Big-Endian\n");

    return 0;
}

IMPORTANT

本课练习题的验证模式是 mode = "return",期望 return 1(小端)——因为 x86 和 ARM 都是小端。但如果代码真正跑在大端机器上,返回 0 也是正确的结果。一个好的判断字节序的程序,应该对两种结果都能正确处理。

2.5 为什么选择小端?为什么选择大端?

这不是一个「哪个更好」的问题——两种设计各有合理的出发点:

优势劣势
小端低字节在低地址,做 intchar 截断时无需改地址;加法和乘法从低字节开始,与内存读取顺序一致阅读内存 dump 时数字是反的
大端内存 dump 中数字以「人类习惯」的顺序显示(高位在前);网络字节序采用大端,与人类阅读顺序一致不同宽度的类型转换需要调整地址

3. 三种检测字节序的方法

3.1 方法 1:union 法(C 标准推荐,本课使用)

endian_union.c
c
union endian_test {
    unsigned char c[4];
    int i;
};

int main(void)
{
    union endian_test u;
    u.i = 1;                  // 写入已知值

    if (u.c[0] == 1)          // 读取第一个字节
        return 1;             // 低字节在低地址 → 小端
    else
        return 0;             // 低字节在高地址 → 大端
}

u.i = 1 的内存表示分别在小端和大端机器上:

小端: [01][00][00][00]  → u.c[0] = 0x01 → return 1 ✓
大端: [00][00][00][01]  → u.c[0] = 0x00 → return 0

为什么用 1 而不是其他值? 因为 1 恰好只在最低有效字节中有 1,其余三个字节全为 0——这让检测变得极其简单,只需判断 u.c[0] 是否非零即可。

3.2 方法 2:指针强转法(简洁但需注意严格别名)

endian_pointer.c
c
int main(void)
{
    int i = 1;

    // 把 int* 强转为 unsigned char*,然后读取其指向的第一个字节
    if (*(unsigned char*)&i == 1)
        return 1;                   // 小端
    else
        return 0;                   // 大端
}

(unsigned char*)&iint* 指针转换为 unsigned char* 指针,然后 * 解引用只读取 1 个字节——恰好是 int 的最低地址的那个字节。代码极简,但需要注意的是:

WARNING

指针强转做类型双关在 严格别名规则(strict aliasing)下是未定义行为。虽然大多数编译器在实际中会「做你想做的事」,但激进优化(如 -O3 + -fstrict-aliasing)下可能产生意外结果。union 法没有这个风险,是 C 标准推荐的安全方式。

3.3 方法 3:位运算法(不依赖类型转换)

endian_bitwise.c
c
int main(void)
{
    int i = 1;

    // 用位运算提取 int 的最低字节:
    // (i >> 0) 把最低字节移到第 0 位,& 0xFF 只保留低 8 位
    unsigned char lowest_byte = (i >> 0) & 0xFF;

    if (lowest_byte == 1)
        return 1;     // 小端
    else
        return 0;     // 大端
}

这种方法不依赖字节序! 它用的是 C 语言的值语义——(i >> 0) & 0xFF 取的是整数 i最低 8 个有效位,与内存布局无关。但这也意味着:它不能用来检测字节序,因为无论大小端,1 的最低 8 个有效位始终是 1

NOTE

方法 3 标注为 // [!code error] 因为它实际上不能检测字节序。它展示了一个常见误解:有些人以为位移运算能检测字节序,但位移是按而非按内存位置操作的——无论字节序如何,1 >> 0 始终是 10x12345678 >> 24 始终是 0x12


4. 函数式宏 #define:编译期的文本魔术

4.1 函数式宏的基本语法与字符串化运算符 #

READEME 的示例中定义了四个调试宏:

debug_macros.c
c
#include <stdio.h>

#define printc(expr) printf(#expr " = %c \n", expr)
#define printi(expr) printf(#expr " = %d \n", expr)
#define printd(expr) printf(#expr " = %f \n", expr)
#define printx(expr) printf(#expr " = %x \n", expr)
//                          ↑
//                     #expr 将参数转为字符串字面量

int main(void)
{
    union { unsigned char c[4]; int i; } u;
    u.i = 0x12345678;

    printx(u.c[0]);   // 展开为 printf("u.c[0]" " = %x \n", u.c[0])
    printx(u.c[1]);   //                      ↑ 字符串自动拼接 ↑
    printx(u.i);
    // 输出:
    // u.c[0] = 78
    // u.c[1] = 56
    // u.i = 12345678

    return 0;
}

#expr 的工作原理:预处理器把 expr 这个宏参数的原始文本用双引号包裹起来,变成一个字符串字面量。相邻的字符串字面量在 C 语言中自动拼接——所以 #expr " = %x \n" 等价于一个完整的格式字符串。

4.2 ## 记号拼接运算符

还有一个双井号 ## 运算符,用于将两个记号拼接成一个新记号:

macro_concat.c
c
#include <stdio.h>

#define PRINT_FIELD(obj, field) \
    printf(#field " = %d\n", obj.##field)
//                                 ↑ ↑
//                          obj 和 field 被拼接为一个标识符

int main(void)
{
    struct point { int x; int y; } p = {10, 20};
    PRINT_FIELD(p, x);   // 展开为 printf("x" " = %d\n", p.x);
    PRINT_FIELD(p, y);   // 展开为 printf("y" " = %d\n", p.y);
    return 0;
}

4.3 多行宏的 do { ... } while(0) 惯用法

如果需要定义一个包含多条语句的宏,直接写可能会出问题:

macro_bad.c
c
// 不好的写法:在 if-else 中展开后会断裂
#define SWAP(a, b)  tmp = a; a = b; b = tmp
//                                        ↑ 分号导致展开后 if-else 断裂

// 正确的写法:用 do-while(0) 包裹
#define SWAP(a, b)  do { typeof(a) tmp = a; a = b; b = tmp; } while(0)
//                ↑                                        ↑
//          成为一个完整的「块语句」,在任何上下文中都安全

为什么必须是 do-while(0) 而不是单纯的 { }

c
// { } 写法的问题:
#define SWAP_BAD(a, b) { typeof(a) tmp = a; a = b; b = tmp; }

if (condition)
    SWAP_BAD(x, y);   // 展开为 if (condition) { ... }; ← 多余的分号
else
    do_something();   // 编译器报错:else 没有对应的 if!

do { ... } while(0) 解决了这个问题——它需要一个结尾分号,所以 SWAP(x, y); 在语法上是一个完整的语句,不会破坏外部的 if-else 结构。

4.4 函数式宏 vs 真函数:适用场景总结

方面#define max(a,b) ((a)>(b)?(a):(b))函数 int max(int a, int b)
类型检查——任何可比较的类型都能用有严格类型检查
副作用max(a++, b++) 展开后 a 和 b 各被求值两次每个参数只求值一次
调试预处理后消失,无法设置断点可 debug、可单步跟踪
代码膨胀每次调用都展开为内联代码(可能增加体积)生成一个函数体,每次调用一条 call
类型通用性天然支持多类型需要写多个重载(C 不支持重载)
适用场景简单的、对性能极度敏感的、需要多类型支持的复杂的、需要类型安全和可调试性的

TIP

一个经验法则:能用函数就用函数,只有函数做不了的事才考虑宏。在 C 语言中宏的正当用途包括:字符串化(#)、记号拼接(##)、编译期常量定义。


5. sizeof 运算符与联合体大小的完整剖析

5.1 union 与 struct 的大小对比

sizeof_compare.c
c
#include <stdio.h>

struct s_example {
    char c;      // 1 字节 → 偏移 0
    int  i;      // 4 字节 → 偏移 4(因为对齐到 4 字节边界)
    short s;     // 2 字节 → 偏移 8
};
// sizeof = 12(含尾部填充)

union u_example {
    char  c;     // 1 字节
    int   i;     // 4 字节 ← 决定 union 大小
    short s;     // 2 字节
};
// sizeof = 4(只取最大成员)

int main(void)
{
    printf("sizeof(struct s_example) = %zu\n", sizeof(struct s_example)); // 12
    printf("sizeof(union  u_example) = %zu\n", sizeof(union  u_example)); // 4
    return 0;
}

内存布局对比

struct s_example (12 字节):
┌────┬───────────┬────┬─────────┬──────┬───────────┐
 c 填充(3B)   i  s 填充(2B)  
 1B    3B 4B  2B    2B
└────┴───────────┴────┴─────────┴──────┴───────────┘
 偏移0   偏移1     偏移4           偏移8   偏移10

union u_example (4 字节):
┌──────────┐
  i / c/s 三者共享——c 只用第 1 字节,s 用前 2 字节,i 4 字节
  (4 字节) │
└──────────┘
 偏移0

5.2 struct vs union 对比总结

structunion
成员存储各自独立,按声明顺序排列全部从同一起始地址开始,共享空间
sizeof各成员大小之和 + 成员间的对齐填充 + 尾部填充max(最大成员的大小) + 可能的对齐填充
所有成员同时有效?是——可以同时读写所有成员否——同一时刻只有一个成员有意义
主要用途打包一组相关数据(如坐标点、学生信息)类型双关、节省内存、协议解析、变体类型
内存开销较大——每个成员都有独立空间极小——只分配最大成员所需的空间

5.3 结构体对齐与 #pragma pack

结构体成员间的填充是由于对齐要求——int 必须放在 4 的倍数的地址上。可以用 #pragma pack__attribute__((packed)) 去掉填充:

packed_compare.c
c
#include <stdio.h>

// 默认对齐
struct normal {
    char c;     // 偏移 0
    int  i;     // 偏移 4(对齐到 4 字节边界)
};
// sizeof = 8

// 取消对齐填充
struct __attribute__((packed)) packed {
    char c;     // 偏移 0
    int  i;     // 偏移 1(紧跟在 c 后面)
};
// sizeof = 5  ← 没有填充

int main(void)
{
    printf("sizeof(normal) = %zu\n", sizeof(struct normal)); // 8
    printf("sizeof(packed) = %zu\n", sizeof(struct packed)); // 5
    return 0;
}

WARNING

取消对齐填充可以减小结构体体积,但可能导致访问未对齐地址的性能损失——x86 上可能稍慢,某些 RISC 架构(如旧版 ARM)上访问未对齐的 int 会直接触发硬件异常。在生产代码中使用 packed 需要谨慎。


6. 字节序的实际应用

6.1 网络字节序与 htonl/htons 家族

TCP/IP 协议规定:所有多字节整数在网络中传输时必须使用大端格式。为此 C 标准库提供了一组转换函数:

函数全称作用
htonsHost TO Network Short16 位:主机序 → 网络序(大端)
htonlHost TO Network Long32 位:主机序 → 网络序
ntohsNetwork TO Host Short16 位:网络序 → 主机序
ntohlNetwork TO Host Long32 位:网络序 → 主机序
network_order.c
c
#include <stdio.h>
#include <arpa/inet.h>

int main(void)
{
    unsigned short port = 0x1234;   // 端口号 (16-bit)
    unsigned int   addr = 0x12345678;

    printf("=== 主机字节序(本机) ===\n");
    printf("port (host)  = 0x%04x\n", port);
    printf("addr (host)  = 0x%08x\n", addr);

    printf("\n=== 网络字节序(大端) ===\n");
    printf("port (net)   = 0x%04x\n", htons(port));
    printf("addr (net)   = 0x%08x\n", htonl(addr));

    // 在小端机器上,htons/htonl 会反转字节序
    // 在大端机器上,htons/htonl 是空操作(no-op)

    return 0;
}

转换的必要性

场景: 小端 PC 发送整数 1 给大端服务器

PC 内存中 1 的存储:  01 00 00 00
直接发送该字节序列:  01 00 00 00

大端服务器收到后解释: 0x01000000 = 16,777,216 完全错误!

正确做法:
PC 先用 htonl(1) 转换: 00 00 00 01
发送转换后的序列:     00 00 00 01
大端服务器收到:       0x00000001 = 1 正确!

6.2 手动实现字节序转换(不依赖库函数)

manual_htonl.c
c
#include <stdio.h>
#include <stdint.h>

// 手动实现 32 位主机序转网络序(大端)
uint32_t my_htonl(uint32_t hostlong)
{
    // 检测字节序
    uint16_t test = 1;
    if (*(unsigned char*)&test == 1)
    {
        // 小端:字节反转
        return ((hostlong & 0xFF000000) >> 24) |
               ((hostlong & 0x00FF0000) >> 8)  |
               ((hostlong & 0x0000FF00) << 8)  |
               ((hostlong & 0x000000FF) << 24);
    }
    // 大端:无需转换
    return hostlong;
}

int main(void)
{
    uint32_t val = 0x12345678;
    uint32_t net = my_htonl(val);

    printf("host: 0x%08x\n", val);
    printf("net:  0x%08x\n", net);    // 小端: 0x78563412, 大端: 不变

    return 0;
}

6.3 从大端字节流中读取整数

read_big_endian.c
c
#include <stdio.h>
#include <stdint.h>

// 从一个 4 字节大端格式的缓冲区中读取 32 位整数
uint32_t read_u32_be(const unsigned char *buf)
{
    return ((uint32_t)buf[0] << 24) |   // 最高字节
           ((uint32_t)buf[1] << 16) |
           ((uint32_t)buf[2] << 8)  |
           ((uint32_t)buf[3]);          // 最低字节
}

int main(void)
{
    unsigned char network_data[] = {0x00, 0x00, 0x00, 0x01};
    uint32_t val = read_u32_be(network_data);
    printf("value = %u\n", val);    // 输出 1(无论主机字节序如何)
    return 0;
}

这种写法不依赖主机的字节序——它强制按大端语义逐字节组装。这正是 ntohl 的等效实现,也是网络编程中的标准范式。

6.4 本课实战:XModem 协议包的 union 实现

xmodem_packet.c
c
#include <stdio.h>
#include <stdint.h>

#define DATA_SIZE 128

// XModem 协议包: 用 union 同时提供「结构体」和「原始字节」两种视角
union xmodem_packet {
    struct {
        uint8_t  soh;            // 01 — 包头开始标记
        uint8_t  seq;            // 包序号 (1~255)
        uint8_t  seq_comp;       // 包序号补码 (0xFF - seq + 1)
        uint8_t  data[DATA_SIZE];// 128 字节的用户数据
        uint8_t  crc_hi;         // CRC 校验高字节
        uint8_t  crc_lo;         // CRC 校验低字节
    } __attribute__((packed)) fields;
    //   ↑ 取消填充:确保 fields 和 raw 字节完全对齐
    uint8_t  raw[1 + 1 + 1 + DATA_SIZE + 2];   // 共 133 字节
};

int main(void)
{
    union xmodem_packet pkt;
    printf("sizeof(union xmodem_packet) = %zu\n", sizeof(pkt));  // 133

    // 从「结构体视角」构建包
    pkt.fields.soh = 0x01;
    pkt.fields.seq = 5;
    pkt.fields.seq_comp = (uint8_t)(~5 + 1);  // 取补码

    // 从「原始字节视角」查看包
    printf("raw[0] (soh)      = 0x%02x\n", pkt.raw[0]);    // 0x01
    printf("raw[1] (seq)      = %d\n",     pkt.raw[1]);    // 5

    // 实际通信中,可以直接用 write(fd, pkt.raw, sizeof(pkt)) 发送
    // write(serial_fd, pkt.raw, sizeof(pkt.raw));

    return 0;
}

这就是 union 在嵌入式/通信编程中的核心价值——一处定义,两种视角。编译器保证 fieldsraw 从同一个地址开始,所以通过 raw 发送的数据,接收方可以用 fields 解析。


7. IEEE 754 浮点数的存储格式

7.1 1.0 的十六进制表示(0x3F800000)

READEME 中提到 1.00 的存储方式为 0x3F800000。这源自 IEEE 754 单精度浮点数标准:

float 1.0 IEEE 754 单精度 (32 ) 表示:

┌───────┬──────────────────────┬──────────────────────────────┐
 符号     指数 (8 bit)      │       尾数 (23 bit)           │
  S  exponent (biased)   │     mantissa (fraction)      │
 1 bit      8 bits         23 bits
└───────┴──────────────────────┴──────────────────────────────┘

1.0 的具体编码:
  S = 0 (正数)
  exponent = 127 (偏置常量) → 二进制 0111 1111
  mantissa = 0 (1.0 的尾数全为 0)

二进制: 0  011 1111 1  000 0000 0000 0000 0000 0000
         S   exponent        mantissa

重新分组(每 4 位一组):
  0011  1111  1000  0000  0000  0000  0000  0000
   3     F     8     0     0     0     0     0

 0x3F800000

用程序验证

float_repr.c
c
#include <stdio.h>

typedef union {
    float f;
    unsigned int i;
} float_bits;

int main(void)
{
    float_bits u;
    u.f = 1.0f;

    printf("1.0f as hex: 0x%08X\n", u.i);
    // 输出: 0x3F800000

    u.f = 2.0f;
    printf("2.0f as hex: 0x%08X\n", u.i);
    // 输出: 0x40000000(指数 +1, 从 127 变为 128)

    u.f = 0.5f;
    printf("0.5f as hex: 0x%08X\n", u.i);
    // 输出: 0x3F000000(指数 -1, 从 127 变为 126)

    return 0;
}

7.2 小端存储时 0x3F800000 在内存中的实际排列

0x3F800000 的字节分解: [3F][80][00][00](大端视角,高→低)

在小端机器上实际存储:
  内存地址(低 高):  [00] [00] [80] [3F]

                       u.c[0]          u.c[3]

即: u.c[0] = 0x00, u.c[1] = 0x00, u.c[2] = 0x80, u.c[3] = 0x3F

NOTE

理解这一点是嵌入式调试的关键能力。当你在 GDB 或调试器中看到内存 00 00 80 3F 时,需要能识别出这是小端存储的 float 1.0。很多调试时间浪费在「这个值是啥?」上,而这就是答案。

7.3 特殊浮点值一览

IEEE 754 定义了几个特殊值,它们的位模式是固定的:

float_specials.c
c
#include <stdio.h>
#include <math.h>

typedef union {
    float f;
    unsigned int i;
} float_bits;

int main(void)
{
    float_bits u;

    // 正无穷 (Infinity): 指数全 1, 尾数全 0, 符号 0
    u.f = 1.0f / 0.0f;
    printf("+Inf: 0x%08X\n", u.i);          // 0x7F800000

    // 负无穷
    u.f = -1.0f / 0.0f;
    printf("-Inf: 0x%08X\n", u.i);          // 0xFF800000

    // NaN (Not a Number): 指数全 1, 尾数非 0
    u.f = 0.0f / 0.0f;
    printf("NaN:  0x%08X\n", u.i);          // 某非零尾数的模式

    // 正零
    u.f = 0.0f;
    printf("+0:   0x%08X\n", u.i);          // 0x00000000

    // 负零(符号位为 1)
    u.f = -0.0f;
    printf("-0:   0x%08X\n", u.i);          // 0x80000000

    // 最小正非规约数 (subnormal)
    u.i = 0x00000001;
    printf("min subnormal: %e\n", u.f);     // 约 1.4e-45

    return 0;
}
十六进制(float)说明
+0.00x00000000所有位为 0
-0.00x80000000仅符号位为 1
+Inf0x7F800000指数全 1,尾数全 0
-Inf0xFF800000符号位 + 指数全 1,尾数全 0
NaN0x7FC00000(典型)指数全 1,尾数非 0

7.4 浮点数拆解实战

float_decompose.c
c
#include <stdio.h>
#include <stdint.h>

typedef union {
    float f;
    uint32_t i;
} float_bits;

void decompose(float val)
{
    float_bits u;
    u.f = val;

    uint32_t sign     = (u.i >> 31) & 1;
    uint32_t exponent = (u.i >> 23) & 0xFF;
    uint32_t mantissa = u.i & 0x7FFFFF;

    printf("value    = %f\n", val);
    printf("hex      = 0x%08X\n", u.i);
    printf("sign     = %u\n", sign);
    printf("exponent = %u (biased), actual = %d\n", exponent, (int)exponent - 127);
    printf("mantissa = 0x%06X (fractional part)\n", mantissa);

    if (exponent == 0xFF && mantissa != 0)
        printf("-> This is NaN\n");
    else if (exponent == 0xFF && mantissa == 0)
        printf("-> This is %s Infinity\n", sign ? "negative" : "positive");
    else if (exponent == 0 && mantissa != 0)
        printf("-> This is a subnormal (denormalized) number\n");
    printf("\n");
}

int main(void)
{
    decompose(1.0f);
    decompose(-3.14f);
    decompose(0.0f);
    decompose(1.0f / 0.0f);
    return 0;
}

8. 严格别名规则(Strict Aliasing)与类型安全

8.1 什么是严格别名

C 语言的严格别名规则(C99 §6.5/7)规定:只能用与对象类型兼容的指针类型去访问该对象。简称「不同类型指针不能指向同一块内存(带少数例外)」。

strict_aliasing_bad.c
c
// 以下代码在 -fstrict-aliasing 下行为未定义
int i = 0x3F800000;
float f = *(float*)&i;   // 用 float* 访问 int —— 违反严格别名

编译器会假设「int*float* 不指向同一块内存」来做优化,导致上述代码产生意外结果。

8.2 union 法是最安全的类型双关

strict_aliasing_good.c
c
// 正确的做法:通过 union(C 标准明确允许)
typedef union {
    int i;
    float f;
} int_float;

int_float u;
u.i = 0x3F800000;
float f = u.f;    // ✓ 通过 union 做类型双关是合法的

8.3 例外:char* 可以别名任何类型

C 标准规定 char*unsigned char* 可以安全地访问任何类型的内存——这就是为什么我们的字节序检测可以用指针强转法(*(unsigned char*)&i)而不用担心严格别名。

c
int x = 42;
// ✓ 字符类型指针是严格别名规则的例外
unsigned char *p = (unsigned char *)&x;
printf("%d\n", p[0]);    // 安全

IMPORTANT

总结类型双关的三种方式的安全等级:

  • union → C 标准允许,最安全
  • char* / unsigned char* 指针 → 严格别名例外,安全
  • 其他类型的指针强转(如 float* 访问 int)→ 违反严格别名,不安全

参考解答

union 法判断大小端(return 模式)
endian_union_return.c
c
union endian_test
{
    unsigned char c[4];
    int i;
};

int main(void)
{
    union endian_test u;

    u.i = 1;

    if (u.c[0] == 1)
        return 1;      // 小端: 低字节在低地址
    else
        return 0;      // 大端: 低字节在高地址
}

核心原理:u.i = 1 后,u.c[0] 恰好指向 int 的最低有效字节。小端时低字节在低地址 → u.c[0] == 1;大端时高字节在低地址 → u.c[0] == 0

带调试打印的完整版(便于观察字节排列)
endian_debug.c
c
#include <stdio.h>

union endian_test
{
    unsigned char c[4];
    int i;
};

int main(void)
{
    union endian_test u;

    // 用一个能看清所有字节的模式来演示
    u.i = 0x12345678;

    printf("u.i = 0x%x\n", u.i);
    printf("Byte 0: 0x%02x\n", u.c[0]);
    printf("Byte 1: 0x%02x\n", u.c[1]);
    printf("Byte 2: 0x%02x\n", u.c[2]);
    printf("Byte 3: 0x%02x\n", u.c[3]);

    if (u.c[0] == 0x78)
        printf("Result: Little-Endian\n");
    else
        printf("Result: Big-Endian\n");

    // 最终用 1 做检测标准
    u.i = 1;
    return (u.c[0] == 1) ? 1 : 0;
}

0x12345678 展示完整字节排列,用 1 做最终判定。小端时逐字节输出 78 56 34 12,大端时输出 12 34 56 78

指针强转法(等效替代方案)
endian_pointer_return.c
c
int main(void)
{
    int i = 1;

    if (*(unsigned char*)&i == 1)
        return 1;      // 小端
    else
        return 0;      // 大端
}

(unsigned char*)&iint* 强转为 unsigned char*,解引用 * 只读取第一个字节。char* 是严格别名规则的例外——这种方法也是安全的。

对照检查:union 是否定义在 main 外部?u.i = 1 写了吗?条件判断用的是 u.c[0] 而非 u.c[1]return 1 表示自己是小端(x86/ARM 的预期结果)?


课堂讨论

  1. union 中能够包含结构体吗?结构体中能够包含 union 吗?(复合嵌套的合法性)
  2. 列举三种能够使用 union 的实际场合,越具体越好。
  3. 为什么 sizeof(union) 等于最大成员的大小而不是所有成员之和?
  4. 如果小端机器要通过网络发送一个 int 给大端服务器,需要什么处理?为什么?
  5. 指针强转做类型双关和 union 做类型双关,哪个更安全?为什么?

讨论答案

Q1: union 能包含 struct 吗?struct 能包含 union 吗?

两者都可以。 C 语言允许 union 和 struct 任意嵌套组合:

nested_union_struct.c
c
// union 包含 struct — 网络数据包解析
union packet {
    struct {
        uint8_t  type;
        uint8_t  seq;
        uint16_t len;
    } hdr __attribute__((packed));
    uint8_t raw[4];            // 作为字节数组直接发送
};

// struct 包含 union — 变体类型设备 ID
struct device {
    char name[32];
    union {
        int   version;          // 版本号(如 v3)
        char  serial[4];        // 序列号(如 "A001")
    } id;                       // 同一字段两种解读,节省 4 字节
};

这种组合在嵌入式系统和网络协议中非常常见——struct 提供语义化字段视图,union 提供跨类型访问和内存节省的能力。著名的 Linux 内核线程结构 task_struct 中包含大量的 struct-inside-union-inside-struct 嵌套。

Q2: 列举三种使用 union 的具体场合

场合 1:内存节省(变体类型 / Tagged Union)

当数据在任何时刻只以一种形态存在时,union 避免了为所有形态都分配空间。这是实现类型系统的基础:

tagged_union.c
c
enum value_type { TYPE_INT, TYPE_FLOAT, TYPE_STR };

struct value {
    enum value_type type;       // 标签:记录当前激活的类型
    union {
        int   i_val;
        float f_val;
        char  s_val[32];
    } data;                     // 三种类型共享同一块空间
};

// sizeof(struct value) ≈ 4 + 32 = 36(而非 4 + 4 + 4 + 32 = 44)

场合 2:协议解析(字节流 ↔ 字段结构)

protocol_parser.c
c
union ethernet_frame {
    struct {
        uint8_t  dst_mac[6];
        uint8_t  src_mac[6];
        uint16_t ether_type;
    } __attribute__((packed)) hdr;
    uint8_t raw[14];    // 发送和接收时用字节数组
};

场合 3:浮点数调试(位模式检查)

float_inspect.c
c
typedef union { float f; uint32_t i; } float_inspector;

void check_float_properties(void)
{
    float_inspector u;
    u.f = -0.0f;
    // 证明负零的符号位为 1
    printf("-0.0f as hex = 0x%08X\n", u.i);  // 0x80000000
}
Q3: sizeof(union) 为什么等于最大成员大小?

因为所有成员共享同一段内存。 union 的设计语义是「可以选择以多种类型之一来解读同一块内存」,但不同时保留所有类型的数据。所以它只需要分配够最大成员的空间:

union_size_reason.c
c
#include <stdio.h>

union example {
    char  c;        // 1 byte
    int   i;        // 4 bytes
    double d;       // 8 bytes  ← 最大,决定 sizeof
};

int main(void)
{
    printf("%zu\n", sizeof(union example));   // 8

    union example u;
    printf("&u.c = %p\n", (void*)&u.c);       // 三个地址
    printf("&u.i = %p\n", (void*)&u.i);       // 完全
    printf("&u.d = %p\n", (void*)&u.d);       // 相同!
    return 0;
}

union 的哲学是「一块内存,多种解读」;struct 的哲学是「各成员独立存储,同时拥有」。对应的内存开销:union 是 O(max),struct 是 O(sum)。

Q4: 小端机器发 int 给大端服务器需要什么处理?

必须用 htonl(或手动字节反转)将主机字节序转为网络字节序(大端),接收方再用 ntohl 转回。

endian_network_demo.c
c
#include <stdio.h>
#include <arpa/inet.h>

int main(void)
{
    int host_val = 1;

    // 错误做法:直接发送主机序
    printf("Direct send of 1: bytes = %02x %02x %02x %02x\n",
           ((unsigned char*)&host_val)[0],
           ((unsigned char*)&host_val)[1],
           ((unsigned char*)&host_val)[2],
           ((unsigned char*)&host_val)[3]);
    // 小端输出: 01 00 00 00  → 大端接收方误解为 0x01000000

    // 正确做法:先转为网络字节序
    int net_val = htonl(host_val);
    printf("After htonl: bytes = %02x %02x %02x %02x\n",
           ((unsigned char*)&net_val)[0],
           ((unsigned char*)&net_val)[1],
           ((unsigned char*)&net_val)[2],
           ((unsigned char*)&net_val)[3]);
    // 输出: 00 00 00 01  → 大端接收方正确解读为 1

    return 0;
}

如果没有转换,发送方值 1(小端字节序列 01 00 00 00)被大端接收方解读为 0x01000000 = 16,777,216——后果可能是灾难性的。

Q5: 指针强转和 union,哪个做类型双关更安全?

union 更安全。 原因如下:

union 法:C 标准(C99 §6.5.2.3 注脚 82)明确允许通过 union 成员访问与上次写入不同的成员。这被视为「安全的类型双关」。编译器的激进优化也不会破坏这种用法。

指针强转法(non-char 类型):违反严格别名规则(strict aliasing, C99 §6.5/7)。编译器假设不同类型指针不指向同一内存,可能在优化时出错:

strict_aliasing_pitfall.c
c
// 违反严格别名——可能被编译器错误优化
int i = 0x3F800000;
float f = *(float*)&i;    // 编译器可能认为 &i 和 (float*)&i 不重叠

char* / unsigned char* 指针强转:这是严格别名规则的例外——可以安全地访问任何类型。本课的指针强转法(*(unsigned char*)&i)因此是安全的。

结论:做类型双关时,优先用 union(最安全),其次用 char* / unsigned char*(安全范围窄但在例外范围内),尽量避免其他类型的指针强转。


课后练习

  1. 指针法验证:不用 union,只用指针强转 *(unsigned char*)&i 完成字节序检测。

    知识点提示:指针类型转换、& 取地址、* 解引用、char* 的严格别名例外

  2. XModem 协议包:用 union 定义一个 XModem 协议包——包含包头 soh(1 字节)、包序号 seq(1 字节)、序号补码 seq_comp(1 字节)、数据区 data[128]、CRC 校验 crc[2],同时可以用 raw[133] 作为原始字节数组发送。

    知识点提示:union 嵌套 struct、__attribute__((packed)) 取消对齐填充、uint8_t 精确宽度类型

  3. 浮点数拆解:用 union 将一个 float 拆解为符号位、指数、尾数,分别打印。

    知识点提示:IEEE 754 单精度格式、union 类型双关、位掩码 & 和位移 >>、特殊值(NaN/Inf)的识别

  4. 手动 htonl:在不使用 <arpa/inet.h> 的情况下,实现自己的 htonl 函数,先检测字节序再决定是否反转。

    知识点提示:先判断大小端 → 如果是小端则按字节反转 → 大端则原样返回

练习1: 指针法验证字节序
ex1_pointer_endian.c
c
#include <stdio.h>

int main(void)
{
    int i = 1;
    unsigned char *p = (unsigned char *)&i;

    if (*p == 1)
        printf("Little-Endian\n");
    else
        printf("Big-Endian\n");

    return (*p == 1) ? 1 : 0;
}

(unsigned char *)&i 将 int 的地址当作 char 的地址看待。解引用 *p 只读取第一个字节(最低地址的字节)。在小端机上这个字节是 0x01,在大端机上是 0x00

练习2: XModem 协议包
ex2_xmodem.c
c
#include <stdio.h>
#include <stdint.h>

#define DATA_SIZE 128

union xmodem_packet {
    struct {
        uint8_t soh;                  // 01 — Start of Header
        uint8_t seq;                  // 包序号 (1~255, 255 后回卷到 0)
        uint8_t seq_comp;             // 序号补码 = 0xFF - seq(用于错误检测)
        uint8_t data[DATA_SIZE];      // 128 字节数据区
        uint8_t crc_hi;               // CRC 校验高字节
        uint8_t crc_lo;               // CRC 校验低字节
    } __attribute__((packed)) fields;
    uint8_t raw[1 + 1 + 1 + DATA_SIZE + 2];  // 共 133 字节
};

int main(void)
{
    union xmodem_packet pkt;

    // 用结构体视角构建
    pkt.fields.soh  = 0x01;
    pkt.fields.seq  = 1;
    pkt.fields.seq_comp = (uint8_t)(~1 + 1);  // 取二进制补码
    // 填充数据...

    printf("soh  = 0x%02x\n", pkt.raw[0]);
    printf("seq  = %d\n",     pkt.raw[1]);
    printf("comp = %d\n",     pkt.raw[2]);

    // 验证: seq + comp 应为 0(补码互为正负)
    printf("verify: %d + %d = %d (should be 0)\n",
           (int)pkt.fields.seq, (int)pkt.fields.seq_comp,
           (int)(pkt.fields.seq + pkt.fields.seq_comp));

    return 0;
}

__attribute__((packed)) 确保结构体成员间没有对齐填充——让 fieldsraw 的字节布局精确一致。这是嵌入式通信的核心技巧。

练习3: 浮点数拆解
ex3_float_decompose.c
c
#include <stdio.h>
#include <stdint.h>

typedef union {
    float    f;
    uint32_t i;
} float_bits;

void print_float_detail(const char *label, float val)
{
    float_bits u;
    u.f = val;

    uint32_t sign  = (u.i >> 31) & 1;
    uint32_t exp   = (u.i >> 23) & 0xFF;
    uint32_t mant  = u.i & 0x7FFFFF;

    printf("[%s] %f\n", label, (double)val);
    printf("  raw hex = 0x%08X\n", u.i);
    printf("  sign=%u  exp=%3u (actual=%4d)  mant=0x%06X\n",
           sign, exp, (int)exp - 127, mant);

    // 识别特殊值
    if (exp == 0xFF && mant != 0)
        printf("  ⚠ This is NaN (Not a Number)\n");
    else if (exp == 0xFF && mant == 0)
        printf("  ⚠ This is %s Infinity\n", sign ? "-" : "+");
    else if (exp == 0 && mant == 0)
        printf("  ⚠ This is %s zero\n", sign ? "-" : "+");
    else if (exp == 0 && mant != 0)
        printf("  ⚠ This is a subnormal (denormalized) number\n");
    printf("\n");
}

int main(void)
{
    print_float_detail("1.0",     1.0f);
    print_float_detail("-3.14",  -3.14f);
    print_float_detail("0.0",     0.0f);
    print_float_detail("-0.0",   -0.0f);
    print_float_detail("inf",     1.0f / 0.0f);
    print_float_detail("NaN",     0.0f / 0.0f);

    return 0;
}

IEEE 754 单精度结构一目了然:1 位符号、8 位指数(偏置 127)、23 位尾数。通过 union 不需要任何指针强转就能获取这些位模式。

练习4: 手动实现 htonl
ex4_my_htonl.c
c
#include <stdio.h>
#include <stdint.h>

// 自检字节序
int is_little_endian(void)
{
    uint16_t test = 1;
    return *(unsigned char*)&test == 1;
}

// 手动实现 32 位主机序转网络序(大端)
uint32_t my_htonl(uint32_t hostlong)
{
    if (is_little_endian())
    {
        return ((hostlong & 0xFF000000) >> 24) |
               ((hostlong & 0x00FF0000) >> 8)  |
               ((hostlong & 0x0000FF00) << 8)  |
               ((hostlong & 0x000000FF) << 24);
    }
    return hostlong;  // 大端:无需转换
}

int main(void)
{
    uint32_t val = 0x12345678;
    uint32_t net = my_htonl(val);

    printf("host (LE view): 0x%08X\n", val);
    printf("net  (BE view): 0x%08X\n", net);

    // 在小端机器上: host=0x12345678, net=0x78563412
    // 在大端机器上: 两者相同

    return 0;
}

先自检字节序:是 int 值为 1short 版本。小端时按字节反转(四个 & 掩码 + 四个位移),大端时原样返回。这就是 htonl 的标准等价实现。


参考资料

"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan

Released under the MIT License.