二进制数据表示
二进制数据表示规定一组位怎样解释成整数、浮点数或文字。同样的 11111011,按 8 位无符号整数解释是 251,按 8 位补码解释则是 -5。调试数值溢出、解析网络数据和处理字符串时,先确认表示规则,才能判断读到的值是否正确。
1. 面试核心
先记住这几条,再读正文- 位模式本身不带类型,整数的符号、位宽和解释方式共同决定取值;n 位补码的范围是
-2^(n-1)到2^(n-1)-1。卡 · 无符号整数与补码 - 硬件能截断结果,不代表语言允许依赖该结果,溢出必须按语言、类型和运算规则判断。
- 大端和小端规定多字节值在字节序列中的顺序,不会反转每个字节内部的位;交换数据要约定格式。卡 · 字节序
- IEEE 754 二进制浮点数用符号、指数和有效数表示数值,有限精度意味着部分十进制小数只能近似保存。卡 · 浮点数与舍入
- 码点、编码单元、字节和用户看到的字符不是同一个计数单位,字符串长度必须说明按什么单位计算。卡 · 文字、码点与字节
2. 无符号整数与补码
位(bit)是一个二进制位,取值为 0 或 1。本文按一个字节为 8 位讨论常见平台与数据格式;某门语言对字节的定义还要查对应规范。
无符号整数把每一位按二的幂加起来。8 位的 00000101 等于 4 + 1 = 5,11111111 等于 255,所以 8 位无符号范围是 0 到 255。
补码用最高位的负权重表示有符号整数。n 位补码的最高位权重是 -2^(n-1),其他位仍按正常的二进制权重相加。对 8 位的 11111011:
-128 + 64 + 32 + 16 + 8 + 2 + 1 = -5
这样就能直接解释正数和负数,而不需要先把所有负数都转换回十进制再判断。
| 8 位位模式 | 无符号解释 | 补码解释 |
|---|---|---|
00000000 | 0 | 0 |
01111111 | 127 | 127 |
10000000 | 128 | -128 |
11111011 | 251 | -5 |
11111111 | 255 | -1 |
在固定宽度内,正数的各位取反再加一,可以得到相反数的补码。例如 5 的 00000101 取反得到 11111010,加一得到 11111011。但最小负数没有同位宽的正数对应:8 位补码能保存 -128,保存不了 128。不能因此认为对任何有符号值取负都安全。
另外两种整数表示也要分清:原码用一位表示符号,其余位表示绝对值;反码对正数保留原表示,对负数把相应正数的全部位取反。按 8 位说明:
| 表示 | 5 | -5 | 零的表示 |
|---|---|---|---|
| 原码 | 00000101 | 10000101 | 正零 00000000、负零 10000000 |
| 反码 | 00000101 | 11111010 | 正零 00000000、负零 11111111 |
| 补码 | 00000101 | 11111011 | 只有 00000000 |
补码没有正负两个零,原先额外的位模式可以用于最小负数。原码更接近“符号加绝对值”的直觉,但正负数相加需要根据符号处理不同情况,不能直接把整串位当无符号数相加。
补码也让加法电路可以统一处理正负数。只保留低 8 位时,00000101 + 11111011 的低位结果为 00000000。这里说明的是固定宽度的位运算模型;具体程序是否允许截断,仍由语言规则决定。
3. 表示范围与语言溢出
溢出表示运算的数学结果超出了相应类型能够表示的范围。判断时先确定运算实际使用的类型,不只看变量声明:小整数可能先提升为更宽的类型,混合类型也可能改变运算规则。
以普通整数加法为例:
| 语言与类型 | 越界时的规则 |
|---|---|
Java 的 int、long 加法 | 按固定位宽保留结果,不自动报告溢出;需要检查时使用明确的检查方法 |
| C++17 无符号整数运算 | 按该无符号类型范围的模运算处理 |
| C++17 有符号整数加法 | 超出结果类型范围是未定义行为,不能依赖某次测试出现的回绕 |
Python 的 int 加法 | 整数精度可增长,受可用资源限制,不按固定机器字宽自动回绕 |
因此,“机器用补码,所以有符号最大值加一一定变最小值”不是跨语言结论。C++17 甚至不要求所有实现都用补码表示有符号整数;本文的补码推导与该语言版本的可移植保证需要区分。
解析长度、数组下标和分配大小时,溢出可能让边界检查失效。例如先计算 count * elementSize,再比较结果是否超限,如果乘法已发生回绕或未定义行为,后面的比较就失去依据。应先按目标类型判断可表示范围,或使用提供明确溢出报告的算术接口。C++ 的类型转换规则见C++ 类型系统。
4. 字节序
字节序规定一个多字节值在按地址递增排列的字节中如何存放。大端序把高位字节放在前面,小端序把低位字节放在前面。
对 32 位整数 0x12345678,从低地址向高地址读取:
| 格式 | 第 1 字节 | 第 2 字节 | 第 3 字节 | 第 4 字节 |
|---|---|---|---|---|
| 大端 | 12 | 34 | 56 | 78 |
| 小端 | 78 | 56 | 34 | 12 |
每个格子都是一个十六进制字节,不是把整个二进制串逐位反转。一个字节内部的 0x12 仍然是 00010010。
使用 Python 的 struct 可以显式验证两种序列,避免让测试依赖本机的字节序:
import struct
value = 0x12345678
print(struct.pack(">I", value).hex()) # 12345678
print(struct.pack("<I", value).hex()) # 78563412
print(struct.unpack(">I", bytes.fromhex("12345678"))[0] == value) # True
> 指定大端,< 指定小端,I 在这里指定 4 字节无符号整数格式。数据交换双方除了字节序,还要约定位宽、符号、字段顺序和填充规则。不能直接把某个 C/C++ 结构体的内存当作稳定协议,因为它还可能包含对齐填充,见C++ 类型系统。
网络协议中的许多数值字段采用大端,也有协议明确采用其他格式。解析时以协议定义为准,不能从“来自网络”推出所有字段都应反转;字符串字节、压缩内容和已经序列化的字段更不应统一交换。
5. 浮点数与舍入
浮点数把数值表示成有效数与指数的组合,以有限位数覆盖较大的数量范围。IEEE 754 的二进制格式中,binary32 包含 1 位符号、8 位指数和 23 位小数字段;binary64 包含 1 位符号、11 位指数和 52 位小数字段。
对 binary32 的正规数,数值可以按下面的式子解释:
(-1)^符号位 × (1 + 小数字段 / 2^23) × 2^(指数值 - 127)
式子中的开头 1 不需要保存在小数字段里,因此正规数共有 24 位二进制有效精度。它只适用于正规数:指数全零、全一时另有规则,用于零、次正规数、无穷大和 NaN。NaN 表示非数值结果,不能用普通的相等比较来判断一个值是不是 NaN。
1.5 的二进制是 1.1,所以符号位为 0,指数存储值为 127,小数字段的最高位为 1、其余为 0,组合后是十六进制 3FC00000。可以再次用显式格式验证:
import struct
encoded = struct.pack(">f", 1.5)
print(encoded.hex()) # 3fc00000
print(struct.unpack(">f", encoded)[0]) # 1.5
这里的 f 使用 IEEE 754 binary32 编码,前缀仍只控制输出字节的顺序。浮点字段怎样解释与这些字节在内存中怎样排列,是两个独立问题。
0.1 与 1.5 不同:1/10 在二进制中是无限循环小数,有限位数无法精确保存。输入转换和后续运算都可能发生舍入,也就是把精确结果映射到可表示的相邻值。常见默认模式是舍入到最近值,恰好居中时选择有效数最低位为偶数的结果;具体数值环境还可能支持其他模式。
print(0.1 + 0.2 == 0.3) # False:常见 binary64 环境
print(1.5 + 0.5 == 2.0) # True
第一行的转换与加法舍入使两边不相等,第二行中的值和结果都能精确表示。这不意味着浮点数不能比较相等,而是比较条件要符合数据来源。测量值通常需要按业务尺度设定绝对或相对误差;金额则可能适合整数最小单位或十进制数。随手加一个固定的 0.000001 不是所有任务的通用答案。
浮点数的间隔还会随数量级变化,不能认为所有相邻值都相差相同的小数。大量累加、大小数相加和相近数相减,都需要考虑精度与运算顺序,而不只是显示时保留几位小数。
6. 文字、码点与字节
码点是 Unicode 编码空间中的一个编号,编码则规定这些编号怎样转换为编码单元或字节。UTF-8 使用 1 到 4 个字节表示一个 Unicode 标量值,UTF-16 使用一个或一对 16 位编码单元。
例如“中”的码点是 U+4E2D,UTF-8 字节为 E4 B8 AD;UTF-16 的编码单元是 4E2D,序列化为字节时还需区分大小端:
text = "中"
print(text.encode("utf-8").hex()) # e4b8ad
print(text.encode("utf-16-be").hex()) # 4e2d
print(text.encode("utf-16-le").hex()) # 2d4e
编码改变的是保存形式,不是把同一个字符换成了另一个字符。若把 UTF-8 字节交给其他规则解码,可能得到错误文字或解码失败;传输数据时应记录并使用正确编码。
用户看到的一个字符还可能由多个码点组成。例如 e 与组合重音可以显示为一个带重音的字形,某些 emoji 也由多个码点组合。因此,字节长度、UTF-16 单元数量、码点数量和用户感知的字符数量可能不同。截断昵称、移动光标或截取网络字节时,必须先决定需要哪一种边界。
7 · 面试题卡片
本页题目与参考回答
7.1 原码、反码和补码有什么区别,补码有什么优势?
考察重点:位权、符号解释与可表示范围。
参考回答
原码用符号位加绝对值,反码对负数采用相应正数各位取反,补码则在固定宽度内取反后加一。 原码与反码存在两个零,补码只有一个零,并能让加法电路统一处理正负数。补码多出的位模式可表示最小负数,例如 8 位范围为 -128 到 127;程序越界后是否回绕、报错或产生未定义行为,仍要按语言和运算类型判断。
7.2 大端和小端有什么区别,解析数据时需要注意什么?
考察重点:多字节顺序、字段格式与平台边界。
相关内容:字节序。
参考回答
大端把高位字节放在低地址或序列前面,小端把低位字节放在前面。 对 0x12345678,大端序列是 12 34 56 78,小端是 78 56 34 12,单个字节内部的位不因此反转。解析还要确认位宽、符号、字段顺序和填充,按协议转换,不能直接套用本机结构体布局。
7.3 浮点数怎样表示,为什么有些小数运算会出现误差?
考察重点:有效数与指数、有限精度和舍入。
相关内容:浮点数与舍入。
参考回答
二进制浮点数用符号、指数和有效数表示数值,有限有效位不能精确保存所有十进制小数。 例如 1.5 可以精确写成二进制 1.1,而 0.1 是无限循环二进制小数。转换和运算都可能舍入,误差还受数值大小与运算顺序影响。比较时根据业务尺度选择误差规则,要求精确十进制语义时再选择相应数值表示。
7.4 一个字符为什么可能占多个字节,字符串长度应该怎样计算?
考察重点:码点、编码单元、字节和用户感知字符。
相关内容:文字、码点与字节。
参考回答
编码决定码点占用的字节或编码单元数,而显示的一个字符也可能由多个码点组成。 “中”在 UTF-8 中占 3 字节,在 UTF-16 中占一个 16 位单元;组合重音和部分 emoji 还能跨多个码点。网络长度按协议规定的字节计算,文本编辑通常需要识别用户感知字符边界,不能用同一个长度函数处理所有要求。
8. 参考资料
- Java SE 17 语言规范:Integer Operations
- C++17 草案 N4659:Fundamental types
- C++17 草案 N4659:Expressions
- Oracle Numerical Computation Guide:IEEE Arithmetic
- Python 3.11 文档:struct
- Python 3.11 文档:Floating Point Arithmetic
- Unicode 16.0:Conformance,Encoding Forms
来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明