Chapter 2: 信息的表示和处理
二进制信号可以被更好地表示, 基于二值信号的电路简单又可靠
单独的位没有用处, 但是使用一串01序列以及某种解释, 就能表示任意有限集合的元素
这里考虑三种数字编码: 无符号整数, 二进制补码, 浮点数编码
2.1 信息存储
大多数计算机使用 8 个比特为 1 字节来作为可寻址的存储最小单位
程序把存储器视为一个非常大的字节数组, 也就是虚拟存储器
存储器的每一个字节都由一个唯一的数字表示, 这称为地址
所有可能地址的集合称为虚拟地址空间
2.1.1 十六进制表示法
二进制四合一
进制转换程序:
1 | char number_to_char(int x) { |
2.1.2 字
(本节于第三版中与 2.1.3 合并)
对于一个字长为 n 位的机器, 虚拟地址的范围为 $2^n - 1$
32 位机器限制内存为位 4GB, 但如今基本都是 64 位的机器
2.1.3 数据大小
C语言中给每个数据类型的字节数随着机器和编译器的不同而不同
2.1.4 寻址和字节顺序
多字节对象被存储为连续的字节序列
如一个 int
型变量 x
地址为 0x100
, 那么 int
的四个字节就是 0x100
, 0x101
, 0x102
, 0x103
由两种存储的方式
- 大端法: 最高有效字节在前
- 小端法: 最低有效字节在前
带来的问题:
- 不同存储方式的机器的信息交流
- 阅读表示整数的字节序列
- 编写规避正常的类型系统的程序时
字节表示程序:
1 | typedef unsigned char* byte_pointer; |
2.1.5 表示字符串
C 中的字符串是以 00 为结尾的字符数组, 每个字符都由某个标准编码表示 (比如 ASCII
)
2.1.6 表示代码
不同的机器使用不同且不兼容的指令和编码格式, 二进制代码很少可以做移植
2.1.7 布尔代数和环
布尔 and
和 xor
分别表示模 2 的乘法和加法, 每个元素都是它自己的加法逆元 a ^ a = 0
将这四种运算 (&
, |
, ~
, ^
) 扩展到位向量上, 对于每一位都进行这些运算
位向量的一个有用的应用就是表示有限集合 &
就是交集, |
就是并集, ~
相当于补集
2.1.8 C 中的位级运算
位运算的一个常见用法是实现掩码运算, 把掩码的特定位置设置成 1, 与 x 进行按位与运算
这样就会得到 x 在这些特定位的值, 其他位为 0
~0
是全为 1 的掩码, 这样的可移植性强于0xFFFFFFFF
2.1.9 C 中的逻辑运算
||
, &&
, !
这些运算认为非零的参数都为 true
, 0 为 False
当且仅当运算两边只有 ${0, 1}$ 时, 逻辑运算和位级运算等价 \
其次逻辑运算具有短路效应:
||
前若为true
, 直接返回true
,||
后的表达式不计算&&
前若为false
, 直接返回false
,&&
后的表达式不计算
2.1.10 C 中的移位运算
x << k
丢弃 k 个最高位, 在最低位添加 k 个 0x >> k
丢弃 k 个最低位- 逻辑右移: 在最高位添加 k 个 0
- 算数右移: 符号位不变, 其后添加 k - 1 个零
几乎所有的编译器都采用算数右移
2.2 整数表示
2.2.1 整数数据类型
2.2.2 无符号和二进制补码编码
$B2U_w$ 表示无符号的二进制, $B2U_w = \Sigma_{i = 0}^{w - 1} x_i 2^i$
$B2T_w$ 表示二进制补码, $B2T_w = -x_{w-1}2^{w-1} +\Sigma_{i = 0}^{w - 2} x_i 2^i$
$B2O_w$ 表示二进制反码, $B2O_w = -(x_{w-1}2^{w-1}-1) +\Sigma_{i = 0}^{w - 2} x_i 2^i$
$B2S_w$ 表示符号数值, $B2S_w = (-1)^{x_{w-1}} \times \Sigma_{i = 0}^{w - 2} x_i 2^i$
二进制补码所能表示的值是不对称的
2.2.3 有符号和无符号数的转换
注意强制类型转换:
1 | int x = -1; |
2.2.4 C 中的有符号和无符号数
类型转换发生:
- 显式类型转换
int(12345u)
- 赋值时, 等号右边会被强制转换为等号左边
printf
时,%d
,%u
,%x
, 为有符号十进制, 无符号十进制, 十六进制- 运算时若出现无符号数据, 则把运算中的所有数据转换为无符号再计算
2.2.5 扩展一个数字的位表示
字长小的整数转换到字长大的整数:
- 零扩展: 在最高位补 0
- 符号扩展(补码使用): 在最高位补最高有效位
2.2.6 截断数字
- 对于无符号截断 k 位: $x \enspace mod \enspace 2^k$
- 对于有符号截断 k 位: 转成无符号之后同上操作, 之后把剩下的数字转成有符号
2.2.7 关于有符号和无符号数的建议
在非必要的情况下不要使用无符号数
2.3 整数运算
2.3.1 无符号加法
若当前整数由 w 位二进制位表示, 两个数加法就是 $(x+y)\enspace mod\enspace2^w$
模数加法形成一种数学结构, 称作阿贝尔群, 可交换可结合, 每个元素有一个加法逆元
对于元素 $x$, 它的加法逆元就是 $2^w-x$
2.3.2 二进制补码加法
设 $x + y = z$
- 正溢出: $z>=2^{w-1}$, $sum = x + y - 2^w$
- 正常: $2^{w-1}>z>=-2^{w-1}$, $sum = x + y$
- 负溢出: $z<-2^{w-1}$, $sum = x + y + 2^w$
2.3.3 二进制补码的非
- $x != -2^{w-1}$, $x$ 的加法逆元是 $-x$
- 否则 $x$ 的加法逆元是它自己
一种执行取非的技术时 ~x + 1
2.3.4 无符号乘法
$x \times y = x \times y \enspace mod \enspace 2^w$
2.3.5 二进制补码乘法
x, y 转成无符号进行乘法, 取后 w 位转成有符号整数
2.3.6 乘以常数
$2^w = 1u << w$ (无符号整数)
$2^w = 1 << w$ (二进制补码)
在不溢出的情况下成立
可以把乘的这个常数进行二进制拆分
对于拆分出的某段 111.111, 设最低位为 m, 最高位为 n
- FormA:
(x << m) + (x << m + 1) + ... + (x << n)
- FormB:
(x << n + 1) - (x << m)
2.3.7 除以 2 的幂
整数除法向 0 舍, 右移运算永远向下舍 -5 / 2 = -2
, -5 >> 1 = -3
以下代码等价于 $x / 2^k$
1 | (x < 0 ? (x + (1 << k) - 1) : x) >> k |
2.3.8 关于整数运算的最后思考
补码是一种很聪明的表示方法, 绝大多数运算都和无符号整数的位级操作类似
数据溢出可能是造成难以调试的 bug 的一个原因
2.4 浮点
2.4.1 二进制小数
$b_mb_{m-1}\cdots b_0.b_{-1}b_{-2} \cdots b_n$ 表示 $d = \Sigma_{i = n}^{m} 2^i d_i$
若将分数转换为小数, 则分母不为 2 的幂 的分数不能被准确表示
2.4.2 IEEE 浮点表示
使用 $V = (-1)^s \times M \times 2^E$ 的形式来表示一个数
- 符号
sign
1 表示负数, 0 表示正数 - 有效数
significand
M 是一个二进制小数 - 指数
exponent
E, 对浮点数加权
包括 1 位 s
, $k$ 位 E, $n$ 位 M
float
: $s = 1$, $k = 8$, $n = 23$ 共 32 位double
: $s = 1$, $k = 11$, $n = 52$ 共 64 位
三种模式:
设 $Bias = 2^{k-1}-1$
-
规格化值: 指数区域不是全 0 也不是全 1
- 设 $k$ 位指数区域表示的数为 $e$, $E = e - Bias$
单精度 $e \in [-126, 127]$, 双精度 $e \in [-1022, 1023]$ - 设 $f$ 为小数区域表示的数, $M = 1 + f$ 这样结合指数可以免费获得一个有效位
- 设 $k$ 位指数区域表示的数为 $e$, $E = e - Bias$
-
非规格化值: 指数区域全零
- $E = 1 - Bias$
- $M = f$
非规格化值由两个目的
- 保证能表示 0
- 用来表示非常接近 0 的数
-
特殊数值: 指数区域全是 1
- $f = 0$, 根据符号位表示正无穷或者负无穷
- $f \not= 0$, 表示
NaN
不是一个数字
2.4.3 数值实例
非规格化值如此定义 $E$ 是为了和规格化值免费多出的一位相适配
如此定义的非规格化值和规格化值是无缝衔接的
2.4.4 舍入
浮点运算只能近似表示实数运算,
所以对于 $x$ 应该有一种系统的方法使它能被转换成浮点数, 这就是舍入
一般有四种舍入方式:
相似的, 这些舍入方法可以使用在二进制数上
2.4.5 浮点运算
浮点加法, 乘法没有结合律, 因为舍入的存在
浮点乘法没有分配律
1 | 3.14 + 1e10 - 1e10 = 0 // 3.14 被舍入 |
浮点运算有单调性 \
- $a \geq b$, 则 $a + c \geq b + c$ \
- $a \geq b$, 则
- $ac \geq bc \enspace (c > 0)$
- $ac \leq bc \enspace (c < 0)$
- $a^2 \geq 0 \enspace (a \not= NaN)$
之前的整数运算因为溢出的缘故没有单调性
2.4.6 C 中的浮点
类型转换:
int -> float -> double
安全
double -> float -> int
不安全
注意浮点数转换成整数的时候数值会向零截断
第三版中删除了 IA32 的内容
2.5 小结
大多数机器使用二进制补码作为整数的表示, 使用 IEEE 标准作为浮点数的表示方式
有限的编码长度可能导致溢出, 舍入和错误的发生
练习选做
2.1 ~ 2.6
简单的进制转换和运算
2.7
打印字符串中编码程序:
1 | const char *s = "mnopqr"; |
2.8 ~ 2.9
简单的代数运算
2.10
1 | a b |
2.11
A. $k$ (下标从 0 开始)
B. 根据上述代码, 如果 *x
和 *y
相同, 异或的结果只能为 0
C. <=
改成 <
2.12
1 | int x = 0x87654321; |
2.13
1 | int bis(int x, int m) { |
2.14
注意分辨逻辑运算和位级运算
2.15
1 | !(x ^ y) |
2.16
注意算术右移和逻辑右移的区别
2.17 ~ 2.22
简单算数
2.23
A. 模拟
B. fun1
提取最后八位(无符号), fun2
提取最后八位(有符号)
2.24
算数
2.25
length - 1
是 11....11111
运算两边出现无符号数时, 都转换为无符号数
所以 for (i = 0; i < UMAX; i++)
, i
会超出数组范围
修改: i < length
2.26
A. 两者的减法可能为负数, 此时出现错误
B. 无符号中, 负数会被认为是很大的数, 大于零而产生错误
C. strlen(s) > strlen(t)
2.27
1 | int uadd_ok(unsigned x, unsigned y) { |
2.28 ~ 2.29
简单算数
2.30
1 | int tadd_ok(int x, int y) { |
2.31
哈哈哈哈哈哈
就算溢出了该等式仍旧成立
2.32
$TMIN$ 的加法逆元是他自己, 要特判
1 | int tsub_ok(int x, int y) { |
2.33 ~ 2.34
简单计算
2.35
- 当 $t=0$ 时, $p < 2^w$, $x \times y = p$ 不会溢出
- $p$ 除以 $x$ 等于 $q$, 余 $r$
- $x \times y = x \times q + r + t 2^w$
- $q=y$, $0 = r + t2^w$, 又 $\lvert r \rvert < 2^w$, 所以 $r = t = 0$
- $r = t = 0$, $x \times y = x \times q$, 显然 $q=y$
2.36
1 | int tmult_ok(int x, int y) { |
2.37
A. 一点没用
B. 不能改 malloc
的代码, 所以只能在溢出的时候返回空
1 | uint64_t required_size = ele_cnt * (uint64_t) ele_size; |
2.38
对于任何值都可以
2.39
$n$ 是最高位时, 1 << n + 1
为 $0$, FormB 可以写成 -x << m
2.40
简单算数
2.41
当 $n = m$ 或者 $n = m + 1$ 时, 使用 FormA, 此时只需要做一次或者两次移位运算
其他情况下, FormB 需要做两次移位运算, FormA 需要更多次移位运算
2.42
1 | int div16(int x) { |
2.43
$M = 31, N = 8$
2.44
A. False, $x = TMIN_{32}$, -2147483648 > 0 || 2147483647 < 0
为 false
B. True, 如果 (x & 7) == 7
, 那么最低三位一定为 111
, 右移 29 位之后成为符号位, 一定为负
C. False, $x = 46341$
D. True
E: False, $x = TMIN_{32}$
F: True, 都会被转换成无符号再计算
G: True, $x \times (-y - 1) + uy \times ux$, 也就是 $-x$
2.45
简单算数
2.46
A. $0.000000000000000000000001100[1100]\cdots$
B. $2^{-10} \times \frac{1}{10}$ 约为 $9.54 \times 10_{-8}
C, D 简单算数
2.47 ~ 2.48
简单算数
2.49
在小数部分的最高位放上 1, 后面 $n-1$ 个 0 最后再放个 1 这样就超出范围了
$2^{n+1}+1$, $n=23$ 时十进制表示为16777217
2.50
简单舍入
2.51
A. 题干中 23 位末尾 0 变 1
B. 只有最后一位 1, $2^{-22} \times \frac{1}{10}$, 约为 $2.38 * 10^{-8}
C. D. 简单算数
2.52
算数
2.53
1 |
2.54
- A. YES \
- B. NO:
float
中只有23位存储小数, 所以需要存储25位的数字时有时会出现问题1
2
3
4
5
6x = 0x01000001;
cout << x << endl;
cout << (int)(float)x << endl;
x = 0x01FFFFFF;
cout << x << endl;
cout << (int)(float)x << endl; - C. NO:
double
范围更大 - D. YES
- E. YES
- F. YES
- G. YES
- H. NO:
1
2f = 1e20, d = 3.14;
cout << d << endl << (f + d) - f << endl;
2.55 ~ 2.57
在正文中已经编译运行, 本机器使用小端法
1 | typedef unsigned char* byte_pointer; |
2.58
1 | int is_little_endian() { |
2.59
1 | int _2_59(int x, int y) { |
2.60
1 | int replace_byte(unsigned x, int i, unsigned char b) { |
2.61
1 | int A(int x) { |
2.62
1 | int int_shifts_are_arithmetic() { |
2.63
一个数不变: x | 0
, x & -1
-1 左移可以构造高位全为 1 的数字
1 | unsigned srl(unsigned x, int k) { |
2.64
1 | int any_odd_one(unsigned x) { |
2.65
统计 1 个数的奇偶性, 根据异或不进位加法的特性
把前 16 位和后 16 位异或起来, 这样后 16 位 1 的个数的奇偶性与先前相同
依次到 8 位, 4 位, 2 位, 1 位, 这样只需要判断最后一位的奇偶性即可
1 | int odd_ones(unsigned x) { |
2.66
- 首先生成一个数字, 使得最高有效的 1 到最低位全是 1
x |= x >> 1
这样最高的两位都是 1
x |= x >> 2
这样最高的四位都是 1
依此类推
x |= x >> 16
达成目标 - 右移一次再加一 (如果
x = 0
则加 0)
不加一再右移是为了防止溢出
1 | int leftmost_one(unsigned x) { |
2.67
A. 左移 32 位的行为未定义
B, C 如下
1 | int int_size_is_32() { |
2.68
1 | int lower_one_mask(int n) { |
2.69
1 | unsigned rotate_left(unsigned x, int n) { |
2.70
如果 x 为正, 那么第 n-1 位直到第 w-1 位都必须为 0
如果 x 为负, 那么第 n-1 位直到第 w-1 位都必须为 1
右移去掉 大于 n-1 位的数, 算数左移回来, 如果运算之后结果不变就是可以表示
1 | int fits_bits(int x, int n) { |
2.71
原代码只提取了位, 并没有转换成 signed
形式
1 | int xbyte(packed_t word, int bytenum) { |
2.72
A. sizeof
运算符返回 unsigned
值, maxbytes - sizeof(val)
结果为负数就会出错
B. maxbytes >= (int)sizeof(val)
即可
2.73
1 | int saturating_add(int x, int y) { |
2.74
1 | int tadd_ok(int x, int y) { |
2.75
signed
:
$$
\begin{align}
&(x - 2^{32}s_x)(y - 2^{32}s_y) \enspace mod \enspace 2^{32} \
= &(xy -2^{32}s_xy - 2^{32}s_yx - s_xs_y2^{64}) \enspace mod \enspace 2^{32} \
= &(xy - s_xy - s_yx) \enspace mod \enspace 2^{32}
\end{align}
$$
所以转换回去只需要 $+ s_xy + s_yx$
1 | int signed_high_prod(int x, int y) { |
2.76
1 | void* another_calloc(size_t nmemb, size_t size) { |
2.77
1 | int x = 1; |
2.78
1 | int divide_power2(int x, int k) { |
2.79
1 | int mul3div4(int x) { |
2.80
把最低两位分开计算
- 此时
upper
部分可以被 4 整除, 之后再乘 3 不会有溢出风险 lower <= 3
可以直接乘 3 除以 4, 注意向 0 舍入的问题
1 | int _2_80(int x) { |
2.81
1 | int _2_81_A(int k) { |
2.82
A. False, x = -2147483648
B. True
C. True
D. True
E. True
2.83
A.
$$
2^{len(y)}Y - y = Y \
Y = \frac{y}{2^{len(y)}-1}
$$
B. $\frac57$, $\frac35$, $\frac{19}{63}$
2.84
1 |
|
2.85
- A.
1 2+1023 11000000
->1 10..01 1100...
- B.
0 bias+n 11..11
n 为第三段的位数 - C. 最小规格化数: $2^{1-bias}$, 倒数为 $2^{bias-1}$
$E = bias-1$, $e = E + bias = 253$,
0 11..1101 000000..
2.86 ~ 2.88
计算题
2.89
- A. True
- B. False (
x - y
可能溢出) - C. True
double
精度很高, 在int
范围内可以有结合律 - D. False 乘法结果为 64 位, 有可能舍入, 没有结合律
- E. False 除零错误
2.90
1 | float u2f(unsigned x) { |
2.91
算数
2.92
1 | typedef unsigned float_bits; |
2.93
1 | float_bits float_absval(float_bits f) { |
2.94
1 | float_bits float_twice(float_bits f) { |
2.95
1 | float_bits float_half(float_bits f) { |
2.96
1 | int float_f2i(float_bits f) { |
2.97
1 | float_bits float_i2f(int x) { |