第 2 章:信息的表示和处理

主题:信息的表示和处理 (Representing and Manipulating Information)
目标:理解计算机如何用位表示整数、浮点数和地址,并掌握溢出、转换、舍入等底层规则。

1. 核心视角

所有数据在机器中本质都是位串。

flowchart LR
    A["位串<br/>Bits"] --> B["无符号整数<br/>Unsigned"]
    A --> C["补码整数<br/>Two's Complement"]
    A --> D["浮点数<br/>IEEE Floating Point"]
    A --> E["地址/指针<br/>Address"]
    A --> F["指令编码<br/>Instruction"]

同一段位串,解释方式不同,含义就不同。

位串 解释方式 结果
11111111 无符号 8 位整数 255
11111111 补码 8 位整数 -1
0x41 字符 'A'

2. 信息存储基础

概念 含义
位 (Bit) 最小信息单位,取值 0 或 1
字节 (Byte) 8 位
字长 (Word Size) 指针数据的标称大小,常见为 64 位
地址 (Address) 内存中字节的位置编号
虚拟地址空间 (Virtual Address Space) 程序看到的地址范围

常见大小:

C 类型 常见 x86-64 大小
char 1 字节
short 2 字节
int 4 字节
long 8 字节
char * 8 字节
float 4 字节
double 8 字节

C 标准只规定最小范围,不保证所有平台大小完全一致。CSAPP 第 2 章默认主要讨论 x86-64 环境。

3. 十六进制

4 个二进制位对应 1 个十六进制位。

二进制 十六进制 十进制
0000 0x0 0
0001 0x1 1
0010 0x2 2
0011 0x3 3
0100 0x4 4
0101 0x5 5
0110 0x6 6
0111 0x7 7
1000 0x8 8
1001 0x9 9
1010 0xA 10
1011 0xB 11
1100 0xC 12
1101 0xD 13
1110 0xE 14
1111 0xF 15

4. 字节序

多字节对象在内存中的字节排列方式称为字节序 (Byte Ordering)。

假设整数:

0x01234567
地址从低到高 字节序 内存排列
小端法 (Little Endian) 低有效字节在前 67 45 23 01
大端法 (Big Endian) 高有效字节在前 01 23 45 67
flowchart LR
    A["整数 0x01234567"] --> B["小端:67 45 23 01"]
    A --> C["大端:01 23 45 67"]

x86-64 使用小端法。调试内存时看到字节顺序反过来,通常就是小端造成的。

5. 位级运算

常用技巧:

目标 表达式
掩码最低 8 位 x & 0xFF
设置最低 8 位为 1 x | 0xFF
清零最低 8 位 x & ~0xFF
判断某些位是否全 0 (x & mask) == 0
交换两个值,不推荐实战使用 x ^= y; y ^= x; x ^= y;

逻辑运算和位级运算不同:

运算 示例 结果特征
逻辑与 x && y 结果只为 0 或 1,有短路
逻辑或 x || y 结果只为 0 或 1,有短路
逻辑非 !x x == 0 时为 1
按位与 x & y 逐位计算,无短路

6. 移位

操作 表达式 规则
左移 x << k 右侧补 0
逻辑右移 x >> k 左侧补 0
算术右移 x >> k 左侧补符号位

右移差异:

x = 10000000

逻辑右移 1 位:01000000
算术右移 1 位:11000000

对无符号数,右移是逻辑右移。
对有符号负数,C 标准未完全保证,但多数机器使用算术右移。

7. 无符号整数

w 位无符号整数的取值范围:

0 <= x <= 2^w - 1

位向量:

x = [x_{w-1}, ..., x_1, x_0]

解释为:

B2U_w(x) = sum(x_i * 2^i), i = 0..w-1

示例:

位串 无符号值
0000 0
0001 1
0111 7
1000 8
1111 15

8. 补码整数

w 位补码整数的取值范围:

-2^(w-1) <= x <= 2^(w-1) - 1

位向量解释为:

B2T_w(x) = -x_{w-1} * 2^(w-1) + sum(x_i * 2^i), i = 0..w-2

4 位补码示例:

位串 补码值
0000 0
0001 1
0111 7
1000 -8
1001 -7
1111 -1

关键点:

名称 4 位示例 w 位一般形式
最小值 TMin 1000 = -8 -2^(w-1)
最大值 TMax 0111 = 7 2^(w-1)-1
-1 1111 全 1
0 0000 全 0

非对称性:补码最小值没有对应的正数。
例如 4 位中 -8 存在,但 +8 不存在。

9. 有符号与无符号转换

C 中有符号数和无符号数转换时,底层位不变,解释方式改变。

flowchart LR
    A["位串 1111"] --> B["unsigned: 15"]
    A --> C["signed: -1"]

转换规则:

转换 规则
int -> unsigned 若负数 x,结果为 x + 2^w
unsigned -> int 若值大于 TMax,结果为 u - 2^w
比较中混用 有符号数通常会转为无符号数

典型坑:

int x = -1;
unsigned y = 0;
printf("%d\n", x < y);   // 通常输出 0

原因:x 被转换为无符号数,-1 变为 UINT_MAX。

10. 扩展与截断

10.1 扩展

类型 方法 示例
无符号扩展 高位补 0 1011 -> 00001011
补码扩展 高位补符号位 1011 -> 11111011

补码符号扩展保持数值不变。

4 位:1011 = -5
8 位:11111011 = -5

10.2 截断

截断就是丢弃高位。

0x12345678 截断为 16 位:0x5678

数学上等价于:

x mod 2^k

截断后如果再按补码解释,符号和值都可能改变。

11. 整数加法与溢出

机器整数运算通常是模运算。

w 位无符号加法:
(x + y) mod 2^w

无符号加法溢出:

条件 判断
s = x + y 溢出 s < x 或 s < y

补码加法溢出:

情况 溢出判断
正数 + 正数 得到负数 正溢出
负数 + 负数 得到非负数 负溢出
一正一负 不会溢出
flowchart TD
    A["补码加法 x + y"] --> B{"x,y 同号?"}
    B -- 否 --> C["不会溢出"]
    B -- 是 --> D{"结果符号是否改变?"}
    D -- 是 --> E["溢出"]
    D -- 否 --> F["未溢出"]

12. 取反与乘法

补码取负:

-x = ~x + 1

特殊情况:

-TMin == TMin

原因:TMin 没有可表示的正数对应值。

乘法:

w 位整数乘法只保留低 w 位

对于乘以 2 的幂:

表达式 可优化为
x * 2^k x << k
x / 2^k,非负数 x >> k
x / 2^k,有符号数 需要考虑向 0 舍入

有符号除以 2 的幂时,编译器常加偏置:

(x + (1 << k) - 1) >> k    // x < 0 时用于向 0 舍入

13. 整数安全问题

问题 例子 风险
无符号下溢 0u - 1 得到很大的数
有符号溢出 INT_MAX + 1 C 中属于未定义行为
混合比较 -1 < 0u 结果可能反直觉
长度计算溢出 n * sizeof(T) 分配空间不足
反向循环 for (size_t i=n-1; i>=0; i--) 死循环

更稳妥的写法:

for (size_t i = n; i > 0; i--) {
    size_t idx = i - 1;
    /* use idx */
}

14. 浮点数表示

IEEE 浮点数 (IEEE Floating Point) 由三部分组成:

flowchart LR
    A["浮点位串"] --> B["符号位 s"]
    A --> C["阶码 exp"]
    A --> D["尾数 frac"]

通用形式:

V = (-1)^s * M * 2^E

常见格式:

类型 总位数 符号位 阶码位 尾数位 Bias
float 32 1 8 23 127
double 64 1 11 52 1023

15. 浮点数三类编码

阶码 exp 尾数 frac 类型 数值
00...0 00...0 零 +0 或 -0
00...0 非 0 非规格化数 (Denormalized) (-1)^s * 0.frac * 2^(1-Bias)
非全 0/非全 1 任意 规格化数 (Normalized) (-1)^s * 1.frac * 2^(exp-Bias)
11...1 00...0 无穷 +∞ 或 -∞
11...1 非 0 NaN 非数值
flowchart TD
    A["查看 exp"] --> B{"全 0?"}
    B -- 是 --> C{"frac 全 0?"}
    C -- 是 --> D["零"]
    C -- 否 --> E["非规格化数"]
    B -- 否 --> F{"exp 全 1?"}
    F -- 是 --> G{"frac 全 0?"}
    G -- 是 --> H["无穷"]
    G -- 否 --> I["NaN"]
    F -- 否 --> J["规格化数"]

16. 浮点舍入

常见舍入模式:

模式 含义
向偶数舍入 (Round to Even) 默认模式,减少统计偏差
向零舍入 截断小数部分
向下舍入 向 -∞
向上舍入 向 +∞

向偶数舍入:

值 保留到整数 结果
1.4 整数 1
1.5 整数 2
2.5 整数 2
3.5 整数 4

正好在中间时,选择最低有效位为偶数的结果。

17. 浮点运算性质

浮点运算近似实数运算,但不满足所有数学性质。

性质 整数/实数 浮点数
加法交换律 成立 通常成立
加法结合律 成立 不一定成立
乘法结合律 成立 不一定成立
分配律 成立 不一定成立
溢出 数学整数无溢出 可能得到 ∞
非法值 无 可能得到 NaN

示例:

(3.14 + 1e20) - 1e20   // 可能得到 0
3.14 + (1e20 - 1e20)   // 得到 3.14

18. C 中的强制转换

转换 规则/风险
int -> float 可能舍入,但不会溢出
int -> double 通常能精确表示 32 位 int
long -> double 可能舍入
float/double -> int 向零舍入,超范围或 NaN 行为需谨慎
float -> double 精度扩展
double -> float 可能舍入或溢出

精度关系:

类型 有效精度
float 约 24 个二进制有效位
double 约 53 个二进制有效位
int 通常 32 位
long x86-64 通常 64 位

因此:

int -> double 通常精确
int -> float 可能不精确
long -> double 可能不精确

19. Data Lab 关联

labs/data-lab 主要训练本章内容。

主题 Data Lab 中常见能力
位级运算 只用 ~ & ^ | + << >> 构造表达式
补码 判断符号、取负、边界值
溢出 实现比较、加法安全判断
掩码 构造全 0、全 1、局部位段
逻辑运算 用位运算模拟 !、条件选择
浮点数 拆分 sign/exp/frac,处理 NaN/∞/非规格化数

20. 常见坑

坑 正确理解
认为位串有固定含义 位串含义取决于解释方式
忘记小端字节序 x86-64 内存中低有效字节在低地址
把逻辑运算和位运算混用 &&/|| 有短路,&/| 是逐位运算
以为有符号溢出会稳定回绕 C 中有符号溢出是未定义行为
混用 signed/unsigned signed 通常会转 unsigned
忘记截断会改变符号 高位丢失后再解释可能变负数
以为浮点数就是实数 浮点数有限、离散、会舍入
期待浮点加法结合律 舍入会破坏结合律