第 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)最小信息单位,取值 01
字节 (Byte)8 位
字长 (Word Size)指针数据的标称大小,常见为 64 位
地址 (Address)内存中字节的位置编号
虚拟地址空间 (Virtual Address Space)程序看到的地址范围

常见大小:

C 类型常见 x86-64 大小
char1 字节
short2 字节
int4 字节
long8 字节
char *8 字节
float4 字节
double8 字节

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

3. 十六进制

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

二进制十六进制十进制
00000x00
00010x11
00100x22
00110x33
01000x44
01010x55
01100x66
01110x77
10000x88
10010x99
10100xA10
10110xB11
11000xC12
11010xD13
11100xE14
11110xF15

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 位为 1x | 0xFF
清零最低 8 位x & ~0xFF
判断某些位是否全 0(x & mask) == 0
交换两个值,不推荐实战使用x ^= y; y ^= x; x ^= y;

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

运算示例结果特征
逻辑与x && y结果只为 01,有短路
逻辑或x || y结果只为 01,有短路
逻辑非!xx == 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

示例:

位串无符号值
00000
00011
01117
10008
111115

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 位补码示例:

位串补码值
00000
00011
01117
1000-8
1001-7
1111-1

关键点:

名称4 位示例w 位一般形式
最小值 TMin1000 = -8-2^(w-1)
最大值 TMax0111 = 72^(w-1)-1
-11111全 1
00000全 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 扩展

类型方法示例
无符号扩展高位补 01011 -> 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 < xs < 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^kx << k
x / 2^k,非负数x >> k
x / 2^k,有符号数需要考虑向 0 舍入

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

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

13. 整数安全问题

问题例子风险
无符号下溢0u - 1得到很大的数
有符号溢出INT_MAX + 1C 中属于未定义行为
混合比较-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
float321823127
double64111521023

15. 浮点数三类编码

阶码 exp尾数 frac类型数值
00...000...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...100...0无穷+∞-∞
11...1非 0NaN非数值
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 位
longx86-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/unsignedsigned 通常会转 unsigned
忘记截断会改变符号高位丢失后再解释可能变负数
以为浮点数就是实数浮点数有限、离散、会舍入
期待浮点加法结合律舍入会破坏结合律