跳转到内容

信息的表示:比特、数与文本

内存里只有高电平和低电平,没有整数、小数或汉字。一切都是比特,差别只在解释方式。本章讲三种最基本的解释——整数的补码、小数的 IEEE 754、文本的 Unicode/UTF-8——以及它们各自的设计动机与常见陷阱:整数溢出、0.1+0.2≠0.30.1 + 0.2 \neq 0.3、乱码的成因。

nn 位无符号整数就是二进制计数,范围 [0,2n−1][0, 2^n-1]。有符号数采用补码(two's complement):最高位的权重是 −2n−1-2^{n-1} 而非 +2n−1+2^{n-1}。以 8 位为例:

[1000 0011]补=−128+2+1=−125.[1000\,0011]_{\text{补}} = -128 + 2 + 1 = -125.

补码的设计动机不是数学趣味,而是让加法器不区分符号:无论操作数解释为有符号还是无符号,同一套加法电路都给出正确结果的低 nn 位。同时补码只有一个零(0000 00000000\,0000),而原码/反码都有 ±0\pm 0 之分。

补码带来两个必须记住的事实:

  • 范围不对称:nn 位有符号整数的范围是 [−2n−1, 2n−1−1][-2^{n-1},\, 2^{n-1}-1]。负数比正数多一个,因此 -INT_MIN 会溢出(它等于自己)。
  • 溢出即回绕:加法溢出在 C 中是未定义行为,在大多数语言中静默回绕。判断溢出要用 __builtin_add_overflow 一类工具,而不是符号检查(有符号溢出后符号可能"看起来对")。

移位运算也依赖解释:左移一位等价于乘 2(在不溢出的前提下);右移时逻辑右移补 0,算术右移复制符号位——对有符号数应使用后者。

IEEE 754 双精度浮点数把 64 位分成三段:

(−1)s×1.m×2e−1023,s∈{0,1},  m 为 52 位尾数,  e 为 11 位阶码.(-1)^s \times 1.m \times 2^{e-1023}, \qquad s\in\{0,1\},\; m \text{ 为 52 位尾数},\; e \text{ 为 11 位阶码}.

关键设计有两点:

  • 规格化与隐藏位:尾数恒以 1 开头,于是那位 1 不必存储,52 位尾数实际提供 53 位精度。
  • 指数偏移:阶码存的是偏移后的无符号数,使得浮点数按位比较的顺序与数值大小顺序一致(同号时),硬件可以复用整数比较器。

精度损失的根源是有限尾数:0.1 在二进制下是无限循环小数 0.00011‾0.0\overline{0011},存入 64 位必然被舍入。所以 0.1+0.2≠0.30.1 + 0.2 \neq 0.3 不是 bug,是表示的必然。工程结论:

  • 货金计算用整数(最小单位)或十进制类型,不用浮点;
  • 比较浮点数用 ∣a−b∣<ε|a-b| < \varepsilon,不用 ==;
  • 浮点加法不满足结合律:(a+b)+c≠a+(b+c)(a+b)+c \neq a+(b+c) 可能成立,并行求和的顺序会影响结果(→ 性能工程与可观测性)。

此外要认识几个特殊值:+0 与 -0、Inf、NaN(任何与 NaN 的比较都为假,包括 NaN == NaN),以及非规格化数(渐进下溢,牺牲精度换取接近零的平滑过渡)。

字符集回答"有哪些字符、各编几号",编码回答"编号如何变成字节"。Unicode 给每个字符一个码点(如 U+4E2D 表示"中"),而 UTF-8 决定它在文件里占哪几个字节。

UTF-8 是变长编码:ASCII 字符占 1 字节(与旧系统完全兼容),拉丁字母外的常用字符占 2–3 字节,中文常用字大多占 3 字节。它的两个精妙性质:

  • 自同步:任何字节的高位模式能表明它是首字节还是 continuation 字节,因此从损坏或任意位置都能重新对齐字符边界;
  • 前缀码:没有任何编码是另一个的前缀,字符串拼接与查找不会产生歧义。

"乱码"几乎都是用错误的编码解释了正确的字节:把 UTF-8 的 3 字节按 GBK 的 2 字节切分,就得到"涓枃"这样的噪声。修复方法是统一约定编码(如今即 UTF-8),而不是猜测。

另一个常见陷阱是字形 ≠ 码点:"é" 可以是单个码点 U+00E9,也可以是 e + 组合变音符 U+0301——肉眼无法区分,字符串比较却不同。比较前需要 Unicode 规范化(NFC/NFD)。

多字节数值在内存中的排列顺序有两种:大端(高位在前,符合人类书写)与小端(低位在前,x86/ARM 默认)。单机内部一致时无感,一旦涉及跨系统交换——网络协议、文件格式、序列化——字节序就必须显式约定。网络字节序是大端,因此有 htonl/ntohl;跨平台文件格式要么固定字节序,要么写入自描述标记(如 BOM)。

overflow.c
#include <stdio.h>
#include <limits.h>
int main(void) {
int x = INT_MAX;
printf("%d\n", x + 1); /* 未定义行为,常见结果为 INT_MIN */
unsigned u = 0;
printf("%u\n", u - 1); /* 回绕:UINT_MAX */
printf("%d\n", (int)(0.1 + 0.2 == 0.3)); /* 0:二进制下 0.1 与 0.2 均被舍入 */
return 0;
}
  • 补码让加法器不区分符号;代价是范围不对称与静默回绕
  • IEEE 754 的精度损失来自有限尾数,不是 bug;浮点加法不满足结合律
  • UTF-8 向后兼容 ASCII 且自同步;乱码 = 用错误编码解释正确字节;字形 ≠ 码点
  • 字节序只在跨系统交换时显现;网络字节序是大端