信息的表示:比特、数与文本
内存里只有高电平和低电平,没有整数、小数或汉字。一切都是比特,差别只在解释方式。本章讲三种最基本的解释——整数的补码、小数的 IEEE 754、文本的 Unicode/UTF-8——以及它们各自的设计动机与常见陷阱:整数溢出、、乱码的成因。
位无符号整数就是二进制计数,范围 。有符号数采用补码(two's complement):最高位的权重是 而非 。以 8 位为例:
补码的设计动机不是数学趣味,而是让加法器不区分符号:无论操作数解释为有符号还是无符号,同一套加法电路都给出正确结果的低 位。同时补码只有一个零(),而原码/反码都有 之分。
补码带来两个必须记住的事实:
- 范围不对称: 位有符号整数的范围是 。负数比正数多一个,因此
-INT_MIN会溢出(它等于自己)。 - 溢出即回绕:加法溢出在 C 中是未定义行为,在大多数语言中静默回绕。判断溢出要用
__builtin_add_overflow一类工具,而不是符号检查(有符号溢出后符号可能"看起来对")。
移位运算也依赖解释:左移一位等价于乘 2(在不溢出的前提下);右移时逻辑右移补 0,算术右移复制符号位——对有符号数应使用后者。
浮点数:IEEE 754
Section titled “浮点数:IEEE 754”IEEE 754 双精度浮点数把 64 位分成三段:
关键设计有两点:
- 规格化与隐藏位:尾数恒以 1 开头,于是那位 1 不必存储,52 位尾数实际提供 53 位精度。
- 指数偏移:阶码存的是偏移后的无符号数,使得浮点数按位比较的顺序与数值大小顺序一致(同号时),硬件可以复用整数比较器。
精度损失的根源是有限尾数:0.1 在二进制下是无限循环小数 ,存入 64 位必然被舍入。所以 不是 bug,是表示的必然。工程结论:
- 货金计算用整数(最小单位)或十进制类型,不用浮点;
- 比较浮点数用 ,不用
==; - 浮点加法不满足结合律: 可能成立,并行求和的顺序会影响结果(→ 性能工程与可观测性)。
此外要认识几个特殊值:+0 与 -0、Inf、NaN(任何与 NaN 的比较都为假,包括 NaN == NaN),以及非规格化数(渐进下溢,牺牲精度换取接近零的平滑过渡)。
文本:Unicode 与 UTF-8
Section titled “文本:Unicode 与 UTF-8”字符集回答"有哪些字符、各编几号",编码回答"编号如何变成字节"。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)。
#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 且自同步;乱码 = 用错误编码解释正确字节;字形 ≠ 码点
- 字节序只在跨系统交换时显现;网络字节序是大端