C++ / a working model

08 / 80   ·   C++11   ·   约 8 分钟

大小端 Endianness:数值与字节顺序分离

先记住这句话

大小端描述多字节标量在存储中的字节排列,不改变数值本身。可移植协议应显式定义字段宽度和编码顺序,再用无符号运算编解码;不要通过未对齐强转或读取未激活的联合体成员猜测端序。

本篇内容
  1. 地址顺序不等于数值顺序
  2. 查询平台,或者绕开依赖
  3. 示例刻意编码,而非复制布局
  4. 运行示例
  5. 动手练习

地址顺序不等于数值顺序

在八位字节且整数采用普通多字节表示的环境里,数值 0x12345678 的大端表示从低地址到高地址依次为 12、34、56、78,小端则反过来。它们表达同一个数值,算术加法或右移运算的结果不会因端序改变。

端序讨论的单位通常是字节,不是把一个字节内的位也倒转。C++ 的字节位数由 CHAR_BIT 决定,网络协议常说的 octet 明确是八位。涉及原始字节传输时必须检查这一前提,不能把所有平台都默认为八位 char。

查询平台,或者绕开依赖

C++20 的 <bit> 提供 std::endian;native 可以等于 little 或 big,也可能两者都不等,代表混合情况。因此只用一个 if 和 else 就假定所有实现非大即小,并不是完整的可移植判断。C++23 又提供整数 byteswap 以翻转字节顺序。

很多协议代码根本不需要查询本机端序:用无符号右移和掩码提取每个八位字段,再按规定顺序保存即可。解码时把字段提升到足够宽的无符号类型后左移,这描述的是数值重建,不依赖宿主对象表示。

示例刻意编码,而非复制布局

示例用至少能容纳三十二位的 unsigned long 保存数值,生成四个范围为零到二百五十五的 unsigned char 元素。即使 char 超过八位,这些元素的数值仍然确定;若真的写到只接受八位 octet 的外部设备,还需要平台传输层满足协议要求。

检查对象表示可以借助字符访问或 memcpy,但不能把字节缓冲区强转后直接解引用为整数:地址对齐、生命期和类型访问规则都可能出错。浮点数还涉及表示格式,仅调换字节不等于获得跨平台通用浮点编码。

容易答错的地方

  • 网络字节序通常约定为大端,但文件格式可能另有规定;必须读取具体协议,不能按网络经验猜文件。
  • 读取联合体中未激活的另一个成员不是标准 C++ 的通用端序检测方法。

运行一个例子

最低标准 C++11 · 完整程序 · 下载 .cpp

#include <array>
#include <cassert>
#include <iostream>

int main() {
    const unsigned long value = 0x12345678UL;
    const std::array<unsigned char, 4> bytes = {{
        static_cast<unsigned char>((value >> 24) & 0xffUL),
        static_cast<unsigned char>((value >> 16) & 0xffUL),
        static_cast<unsigned char>((value >> 8) & 0xffUL),
        static_cast<unsigned char>(value & 0xffUL)
    }};
    unsigned long restored = 0;
    for (unsigned char b : bytes) restored = (restored << 8) | b;
    assert(restored == value);
    assert(bytes[0] == 0x12 && bytes[3] == 0x78);
    std::cout << static_cast<unsigned>(bytes[0]) << ' '
              << static_cast<unsigned>(bytes[3]) << '\n';
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-endianness.cpp -o example && ./example

预期结果

18 120

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

协议的两个八位字段依次是 0x01、0x02,约定大端无符号整数。宿主是小端时,应如何解码,结果是多少?

查看参考答案

先保证两个字段值在 0..255,使用 (static_cast<unsigned long>(first) << 8) | second,结果是 258。因为表达式操作数值而不是复制宿主表示,小端机器无需先调换数组元素。若直接 memcpy 到整数,其结果才会依赖宿主端序以及目标整数大小。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录