# 数据的表示和运算
## 数制和编码
- 进位计数制及其数据之间的相互转换
- 定点数的编码表示
## 运算方法和运算电路
- 基本运算部件
- 加/减运算
- 乘/除运算
## 整数的表示和运算
- 无符号整数
- 带符号整数
## 浮点数的表示和运算
- IEEE 754标准
- 浮点数的加/减运算
数据表示和计算
1 - 整数的表示
BCD 码
BCD(Binary-Coded Decimal)码是一种 二进制编码方法,用于表示 十进制数字。每个十进制数字(0-9)都使用 四位二进制数字 表示。
BCD 码的基本思路是 单独表示每个十进制数字的二进制值,而不是像传统的二进制数系统那样对整个数字进行编码。例如,在传统的二进制编码中,数字"19"会表示为 10011,但在 BCD 中,它被表示为两个独立的数字:“0001”(对应于 1)和“1001”(对应于 9),因此整个表示为“0001 1001”。
0 - 0000 1 - 0001 2 - 0010
3 - 0011 4 - 0100 5 - 0101
6 - 0110 7 - 0111 8 - 1000
9 - 1001
BCD 编码在某些应用中是很有用的,特别是在需要与 十进制界面 进行交互的地方,如 数字显示 或某些早期的计算机系统。尽管它不如 纯二进制编码 效率高,但它简化了与十进制数据的转换过程。
整数的表示
在介绍整数的不同表示之前,我们首先要定义清楚不同的名词,否则会造成极大的概念混淆。
在日常生活中,我们使用十进制表示数字,并通过符号(如 + 和 -)表示正负。这种用十进制表达的数字也叫做 字面值。
而计算机以二进制形式存储数据,补码、原码、无符号数 等只是 对二进制位的一种解释方式,它们决定了同一组二进制位所代表的实际数值。
在实际的计算机中,没有采用 原码 存储数据的,具体原因后面会谈到到。对于计算机中存储的数字来说,它的二进制只可能是 有符号数(signed) 或者 无符号数(unsigned) 这两种情况。
无符号数
无符号数(unsigned number) 是计算机中一种整数类型,只能表示 非负数(包括 0 和正整数),不包含负数。
纯二进制表示
在非负数中,数字中的第 $n$ 位的大小就是 $2^{n}$。
对于一个 $n$ 位 无符号数 $U$,假设它的第 $i$ 位示为 $b_{i}$,则该数字的二进制表示为 $U_{\text{binary}} = b_{n-1} \cdots b_{1} b_{0}$,对应的十进制数为:
$$U_{\text{decimal}} = \sum_{i=0}^{n-1} 2^{i} \cdot b_{i}$$
纯二进制表示范围根据以上公式可知,一个 $n$ 位纯二进制表示的:
- 最小值:$0$
- 最大值:$2^n - 1$
- 表示范围:$[0, 2^n - 1]$
需要注意的是,无符号数 就是没有符号的二进制表示,它不像 有符号数 的表示,有一个 补码 这个专有名词。但是我们一般可以叫它 纯二进制 或 无符号数二进制表示。
二进制 → 十进制
这里为了方便说明如何将 unsigned 转化为 十进制字面值,采用 8 位 无符号数 进行说明,其中每一位表示的实际大小如下表所示:
| 第 7 位 | 第 6 位 | 第 5 位 | 第 4 位 | 第 3 位 | 第 2 位 | 第 1 位 | 第 0 位 |
|---|---|---|---|---|---|---|---|
| $2^7 = 128$ | $2^6 = 64$ | $2^5 = 32$ | $2^4 = 16$ | $2^3 = 8$ | $2^2 = 4$ | $2^1 = 2$ | $2^0 = 1$ |
10000001对应的值为 $128 + 1 = 129$10001001对应的值为 $128 + 8 + 1 = 137$01000001对应的值为 $64 + 1 = 65$
在现代 64 位计算机中,unsigned 是 32 位非负数。也可以使用 uint16_t 表示 16 位非负数、uint32_t 表示 32 位非负数、uint64_t 表示 64 位非负数。
十进制 → 二进制
将 十进制字面值 转化为 无符号数 的方法就是使用 $U_{\text{decimal}} = \sum_{i=0}^{n-1} 2^{i} \cdot b_{i}$ 进行反向转换。
如果数字不太复杂,可以利用直觉直接观察,将一个数字转化为若干个 2 的 n 次方之和(1, 2, 4, 8 …)。比如 $73 = 64 + 8 + 1 = 2^6 + 2^3 + 2^0$,如果该数字为 8 位的话,那么该十进制对应的二进制就是 01001001。
第二种方法稍微麻烦点,就是充当人脑计算机,采用 除 2 取余法 逐步计算余数:
73 ÷ 2 = 36 ... 1
36 ÷ 2 = 18 ... 0
18 ÷ 2 = 9 ... 0
9 ÷ 2 = 4 ... 1
4 ÷ 2 = 2 ... 0
2 ÷ 2 = 1 ... 0
1 ÷ 2 = 0 ... 1
接下来将余数进行逆序重排得到二进制数:1001001,因为是 8 位数,所以在剩下的高位全部填上 0,得到结果 01001001。
总结两种转换方式如下图:
有符号数
在程序中使用 short、int、long 定义的整形变量就是 有符号数(signed),有符号数使用 补码(Two’s complement) 进行表示。
补码表示
有符号数 使用的 补码 与 无符号数 使用的 纯二进制 表示方式基本相同,除了最高位。对于一个 $n$ 位补码,其最高位为 $-2^{n-1}$ 是一个负数,而不是 $2^{n-1}$。
对于一个 $n$ 位 有符号数 $S$,假设它的第 $i$ 位表示为 $b_{i}$,则该数字的二进制表示为 $S_{\text{binary}} = b_{n-1} \cdots b_{1} b_{0}$,对应的十进制数为:
$$S_{\text{decimal}} = \sum_{i=0}^{n-2} 2^{i} \cdot b_{i} - 2^{n-1} \cdot b_{n-1}$$
补码的表示范围根据以上公式可知,一个 $n$ 位 补码 的:
- 最小值(最负数):$-2^{n-1}$
- 最大值(最正数):$2^{n-1} - 1$
- 表示范围:$[-2^{n-1}, 2^{n-1} - 1]$
那么 补码 的命名为什么叫 Two’s Complement 呢?
对于表示范围内的正数,补码 和 纯二进制表示 的二进制表示是相同的;但是对于负数,两者的二进制表示不同,这也是它名称的来源。
Two’s Complement 中的 Two 强调二进制系统;比如十进制中有 10’s complement(十的补码),那么在二进制中自然就是 2’s complement。
Complement 指 补数 的数学概念。在数学中,一个数 加上 它的补数 等于某个基数的幂。比如对于数字 $x$,其在二进制 $n$ 位补码表示中的补数定义如下:
$$x\ \text{\small 的二进制补数} = 2^n - x$$
所以如果给定了一个 十进制字面值,我们可以先计算其 纯二进制表示,再进行二进制减法,将 $2^n$ 减去这个数得到相应的 补码表示。
比如假设给定一个十进制 $D$,首先我们可以通过 除 2 取余法 计算其 纯二进制表示 $U_{binary} = u_{n-1} \cdots u_{1}u_{0}$,然后再用 $2^n$ 减去这个数:
以上二进制减法等同于对 $U$ 进行 取反加一 运算,即 $D$ 的 补码 $S$ 可以被表示为:
$$S = 2^n - U = \bar{U} + 1$$
二进制 → 十进制
这里为了方便说明,以 8 位 补码 为例说一下如何将 补码表示的二进制 转化为实际十进制数字。
首先,8 位补码中的每一位的大小为:
| 第 7 位 | 第 6 位 | 第 5 位 | 第 4 位 | 第 3 位 | 第 2 位 | 第 1 位 | 第 0 位 |
|---|---|---|---|---|---|---|---|
| $-2^7 = -128$ | $2^6 = 64$ | $2^5 = 32$ | $2^4 = 16$ | $2^3 = 8$ | $2^2 = 4$ | $2^1 = 2$ | $2^0 = 1$ |
例子:
10000001对应的值为 $-128 + 1 = -127$10001001对应的值为 $-128 + 8 + 1 = -119$01000001对应的值为 $64 + 1 = 65$
以 8 位二进制数为例:
- 对于正数,例如 +5,其二进制表示为 00000101,补码 也是 00000101。
- 对于负数,例如 -5,首先写出 5 的二进制表示:00000101。
- 按位取反得到:11111010
- 加 1 得到:11111011
- 所以,-5 的 补码 是 11111011。
十进制 → 二进制
这里仅讨论如何将一个 十进制负数 转化为 补码二进制,因为对于非负数,补码 的二进制表示和 纯二进制 相同。
一种比较简单的转换方法就是基于 $S_{\text{decimal}} = \sum_{i=0}^{n-2} 2^{i} \cdot b_{i} - 2^{n-1} \cdot b_{n-1}$ 这个公式进行直接观察。
比如 $-73 = -128 + 55 = -128 + 32 + 16 + 4 + 2 + 1 = -2^7 + 2^5 + 2^4 + 2^2 + 2^1 + 2^0$,对应的 补码 为 1011 0111。
另一种就是先计算该数字绝对值的 纯二进制表示,再将其通过 取反加一 的方式转化为 补码。
比如通过第二种方式将 -73 转为 补码:
- -73 是负数,其绝对值为 73。
- 通过 除 2 取余法 得到其 无符号表示 为 0100 1001。
- 取反:1011 0110。
- 加一:1011 0111。
结果为 1011 0111。
优点
使用 补码 可以使得 加法、减法和负数的表示 变得简单和统一。比如给定两个数字 A 和 B,我们想要计算 C = A + B。A + B 由硬件加法器得到的二进制结果可以直接作为 C 的二进制表示。
补码 的设计使得负数的加法变得简单。当使用 补码 表示时,可以直接将两个数相加,即使其中一个数是负数,而不需要进行任何特殊处理。这简化了计算机硬件的设计。
原码
原码(Sign-Magnitude) 是一种简单的方式来表示二进制中的 有符号整数。在这种表示法中,数字的 最高位(最左边的位)用于表示符号,其余的位表示数字的大小。通常,符号位为 0 表示正数,而 1 表示负数。
表示方法
原码的表示方法:
- 正数的原码:最高位 为 0,其余位表示这个数的绝对值的二进制形式。
- 负数的原码:最高位 为 1,其余位表示这个数的绝对值的二进制形式。
- 零的原码:符号位可以是 0(表示 +0)或 1(表示 -0),但在实际应用中,通常只使用一个零,即符号位为 0。
举例(以 8 位二进制数为例):
- +25 的原码是:
0 0011001 - -25 的原码是:
1 0011001
缺点
- 存在 正零 和 负零 的表示。
- 在进行算术运算时,正数和负数需要不同的处理,这使得硬件设计变得复杂。
由于上述的缺点,原码 并不是计算机中最常用的表示法。在现代计算机中,补码 是更常用的方式来表示 有符号整数,因为它简化了算术运算的处理,并且没有 +0 和 -0 的冗余表示。
2 - 浮点数表示
实数的二进制表示
实数在计算机中的存储遵循 IEEE 浮点数标准,但在这里为了方便理解 IEEE 标准,这里首先阐明一般 实数 在计算机中是如何存储的。
在这里 实数 分为两个部分存储:整数部分 和 小数部分,两个部分在逻辑上用 · 隔开。
比如对于以下 浮点数 的二进制表示:
$$d_m d_{m-1} \cdots d_1 d_0 \cdot d_{-1} d_{-2} \cdots d_{-n}$$
其中每一位对应的数值如下表所示:
| $d_m$ | $d_{m-1}$ | $\cdots$ | $d_1$ | $d_0$ | $d_{-1}$ | $d_{-2}$ | $\cdots$ | $d_{-n}$ |
|---|---|---|---|---|---|---|---|---|
| $2^m$ | $2^{m-1}$ | $\cdots$ | $2$ | $1$ | $1/2$ | $1/4$ | $\cdots$ | $1/2^{n}$ |
上述二进制表示对应的数值为
$$b = \sum_{i=-n}^{m} {2^{i} \times b_{i}}$$
其中 $b_i = 0$ 或 $b_i = 1$,表示第 i 为是 0 还是 1。
举例说明如何使用上述表示法计算 实数:
- $101.11_{2}$ 对应的数值为 $1 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 + 1 \times 2^{-1} + 1 \times 2^{-2} = 5.75$
- $1011.1_{2}$ 对应的数值为 $8 + 0 + 2 + 1 + \frac{1}{2} = 11.5$
字面值转二进制
举个实际的例子,将 1.2 转换为二进制表示。
整数部分 1 的二进制是 1。
小数部分 0.2 的二进制表示是一个无限循环小数。通过不断乘以 2,可以得到近似的二进制:
- 0.2 × 2 = 0.4 → 整数部分 0
- 0.4 × 2 = 0.8 → 整数部分 0
- 0.8 × 2 = 1.6 → 整数部分 1
- 0.6 × 2 = 1.2 → 整数部分 1
- 0.2 × 2 = 0.4 → 重复…
于是,1.2 在二进制中近似为 1.0011001100110011...。
IEEE 浮点数表示
上述 浮点数 存储方式的缺点在于如果要表示比较大的数,就需要比较多的二进制位数,比如对于 $5 \times 2^{100}$ 就需要 103 位。IEEE 表示 就解决了这个问题,在 IEEE 表示 中,浮点数 $V$ 被表示为 $V = (-1)^{s} \times M \times 2^{E}$,其中 $s, M, E$ 的含义如下:
- $s$ 为 符号位
- $M$ 为 乘法因子
- $E$ 为 指数部分
IEEE 754 标准 是定义浮点数表示和算术的国际标准,它定义了多种不同精度的浮点数格式,但最常见的是 单精度 single precesion(32 位)和 双精度 double precesion(64 位),浮点数分为 s(符号位)、exp(阶码)、frac(尾数)三个部分存储:
$1$ 位的 $s$ 与上述计算公式中的 符号位 $s$ 相同,位于浮点数二进制表示的 最高位,标志了浮点数的正负,如果 $s = 1$ 的话 $V$ 是负数,如果 $s = 0$ 的话 $V$ 为正数
$k$ 位的 $exp = e_{k-1} \cdots e_1 e_0$ 用于计算 指数部分 $E$($exp$ 为 unsigned 值,但不能为全 0 或全 1),其中 $E = exp - Bias$,$Bias = 2^{k-1} - 1$
- 对于 单精度 浮点数
- $k = 8$,$Bias = 2^{8-1} - 1 = \textbf{127}$
- $1 \le exp \le 254$
- $-126 \le E \le 127$
- 对于 双精度 浮点数
- $k = 11$,$Bias = 2^{11-1} - 1 = \textbf{1023}$
- $1 \le exp \le 2046$
- $-1022 \le E \le 1023$
- 对于 单精度 浮点数
$n$ 位的 $frac = 0 \cdot f_{n-1} \cdots f_1 f_0$ 用于计算 因子 $M$,其中 $M = 1 + frac$,$frac$ 表示的小数计算方式见 实数的二进制表示
- 对于 单精度 浮点数,$n = 23$,$M$ 的最大值为 $2 - 2^{-23}$,$M$ 最小值为 $1$
- 对于 双精度 浮点数,$n = 52$,$M$ 的最大值为 $2 - 2^{-52}$,$M$ 最小值为 $1$
总结 单精度 和 双精度 浮点数 表示如下:
| 类型 | 符号位 s | 阶码 exp | 尾数 frac | 总位数 | 偏置值 |
|---|---|---|---|---|---|
| 单精度 | 1 | 8 | 23 | 32 | 127 |
| 双精度 | 1 | 11 | 52 | 64 | 1023 |
单精度 浮点数表示为:
$$(-1)^s \times 1.\text{frac} \times 2^{\text{exp} - 127}$$
双精度 浮点数表示为:
$$(-1)^s \times 1.\text{frac} \times 2^{\text{exp} - 1023}$$
总结 单精度 和 双精度 浮点数中各个字段的范围如下图所示:
异常值
注意 IEEE 浮点数 表示分为 Normalized Values(正常值)和 Denormalized Values(非正常值)以及特殊值,上节中提到的 IEEE 浮点数 计算方法只适用于 正常值,
正常值 的 阶码(exp)不能为全 0 或全 1。
下图是 浮点数 各种类型的图示(以 单精度 为例):
- 非正常值(Denormalized)的 阶码全为 0
- 无穷大(Infinity)的 阶码全为 1,尾数位为 0
- 非数字(NaN,Not a Number)的 阶码全为 1,尾数位不为 0
需要注意的是 非正常值 的 浮点数 计算公式 与 正常值 不同:
$$\text{value} = (-1)^{\text{sign}} \times 0.f \times 2^{1 - \text{bias}}$$
注意这里的:
- 没有隐含的 “1”,因为它是 非正规数。
- 指数固定为 $1 - \text{bias}$(而不是 $e - \text{bias}$,因为 阶码是全 0)
- 这是为了在非常接近 0 的地方保持精度连续性(即从最小正规数向 0 过渡的“填充区”)
下表总结了给出了 浮点数 表示各种情况的有效值计算:
| 类型 | 阶码(exp) | 尾数(fraction) | 有效值公式 |
|---|---|---|---|
| 正常值 | 非全 0、非全 1 | $f$ | $± 1.f \times 2^{e - \text{bias}}$ |
| 非正常值 | 全 0 | $f$(必须非 0,否则是 $0$) | $± 0.f \times 2^{1 - \text{bias}}$ |
| $± 0$ | 全 0 | 全 0 | $± 0$ |
| $±\infty$ | 全 1 | 全 0 | $± \infty$ |
| NaN | 全 1 | 非 0 | Not a Number(结果非法或未定义) |
字面值转二进制
前文已经提到如果我们有 浮点数 的 IEEE 二进制表示,如何将其转化为实际的 浮点数,就是通过如下的公式:
$$(-1)^s \times 1.\text{frac} \times 2^{\text{exp} - \text{bias}} $$
还有一种常见的考题是给定我们一个 浮点数字面值,比如 2.25,然后让我们去反推它的二进制表示,这里有没有什么简便的解法呢?
最简便的方法还是反向转换,即将一个 浮点数 表示为 一点几几($1.\text{frac}$)乘以二的多少次方($2^{n}$)的格式,有这两部分可以分别计算出 阶码和尾数,然后符号位由数字的正负判断。
以 2.25 为例,如果我们想得到该 浮点数 的 单精度 表示:
$$2.25 = 1.125 \times 2^{1} = \frac{9}{8} \times 2^1 = (1 + \frac{1}{8}) \times 2^1 = (1 + 2^{-3}) \times 2^1$$
由此可以计算出 浮点数 的各个部分:
- 符号位 为 0
- 阶码 为 $1 + 127 = 128$,二进制表示为
1000 0000B - 尾数 为
0010 0000 0000 ....
所以 浮点数 的二进制为 0100 0000 0001 0000 ...,十六进制为 40100000H。
一般而言,试题只会考察这种没有精度损失的 浮点数二进制转换,对于有精度损失的情况,由于比较繁琐,基本不会考察,其 尾数 部分的计算与 一般实数字面值转二进制 相同。
表示精度
上述的例子是一个比较理想的例子,2.25 可以精确地用 浮点数 表示。但在真实场景中,很多 实数 都是无法精确地用 浮点数 表示的。比如 1.2 这个数:
$$1.2 = (1 + \frac{1}{5}) \times 2^1$$
其中 $\frac{1}{5}$ 无法表示为若干个 $\frac{1}{2^n}$ 之和,所以对于这种情况,我们只能尽量地去接近这个数。
若要理解如何接近这个数,我们首先要理解 精度 的概念。这里我们首先从简单的例子出发,假设 阶码只有 3 位,那么这些 尾数可以被精确表示:
$$\sum_{i=0}^{3} f \times 2^i, f \in {0, 1}$$
在数轴中对应 [0, 1] 区间中的 8 个点:
如果一个尾数的大小与这些点都不相同的话,则需要找一个临近的点来近似,这也是导致 精度 丢失的原因:尾数的二进制表示法无法精确地表示 [0, 1] 中的每一个 实数,对于无法精确表示的,只能去近似。
但是这种误差可以随着 尾数 位数的增加而不断减小,比如对于 单精度浮点数 表示,尾数(frac)为 23 位,可以精确表示以下这些小数:
$$0, \frac{1}{2^{23}}, \frac{2}{2^{23}}, \frac{3}{2^{23}}, \cdots, \frac{2^{23} - 1}{2^{23}}, 1$$
双精度浮点数 表示,尾数为 52 位,可以精确表示以下这些小数:
$$0, \frac{1}{2^{52}}, \frac{2}{2^{52}}, \frac{3}{2^{52}}, \cdots, \frac{2^{52} - 1}{2^{52}}, 1$$
所以 尾数位数越多,精度越高,用以近似表示某些 实数 时,误差更小。
舍入 是在数值计算中将一个数字转换为特定精度的过程。由于计算机中的 浮点数 表示有限,许多数学运算结果不能完全精确地用 浮点数 表示,因此需要 舍入 来逼近这些结果。
浮点数加减
浮点数 加减计算过程包含以下几个步骤:对阶、尾数加减、尾数规格化。
- 对阶:为了进行加减运算,两个 浮点数 必须具有相同的指数,这里采用低阶向高阶对齐的原则,过程如下:
- 比较两个浮点数的 指数。
- 将较小指数的浮点数的 尾数右移,直到两个指数相等。
- 右移过程中,需要注意尾数的 精度损失(尾数右移时可能会丢失低位精度)。
- 尾数加减:在指数对齐后,直接对两个 浮点数的 尾数进行加减。
- 由于尾数已经对齐,可以直接进行 加减操作。
- 根据操作结果可能需要处理进位或借位。
- 尾数规格化:确保结果符合标准化浮点数的格式,即尾数以 1 开头。
- 如果结果尾数不符合 1.xxxx 格式,则需要进行 规格化调整。
- 左移尾数并相应减少指数,或右移尾数并相应增加指数。
- 规格化后,可能还需要进行 舍入,以符合尾数的位数限制。
- 根据舍入模式(如“向偶数舍入”、“向零舍入”等)完成必要操作。
在 对阶 和 尾数规格化 的过程中,由于可能存在 尾数右移,所以可能会导致 精度缺失。
因为 IEEE 浮点数尾数的位数是有限的,如果右移的过程中尾数中最右边的 1 被清除,就会导致 精度缺失。
举一个实际的例子来说明一下,假设 $A = 1.625 × 2^3 = 1.101_{2} × 2^3$,$B = 1.75 × 2^1 = 1.11_{2} × 2^1$,在计算 $A+B$ 时,使用以下步骤:
- 对阶:低位向高位,$B = 0.0111_{2} \times 2^3$。
- 尾数相加:$A + B = (1.101_{2} + 0.0111_{2}) × 2^3 = 10.0001_{2} × 2^3$
- 尾数规格化:$10.0001_{2} × 2^3 = 1.00001 × 2^4$
3 - 运算电路
加法运算电路
半加器
最基本的加法单元是 半加器(Half Adder)。它有两个输入,一个是加数,一个是被加数,并有两个输出,一个是和,一个是进位。
如上图所示,通过对两个输入(A 和 B)进行 异或(XOR)计算,可以得到 和(S)。
通过对两个输入进行 与(AND)操作,可以得到 进位(C,即 Carry)。
半加器 的主要限制是它只能对两个位进行加法,并且不能处理来自低位的进位输入。
全加器
全加器(Full Adder)是 半加器 的扩展,它加上了前一位的 进位(Cin)作为第三个输入,并有两个输出,一个是 和(S),一个是 进位(C)。
半加器用于处理两个位的简单加法,而 全加器则可以处理包括 进位 在内的三位加法,是构建复杂加法电路的基础。
加法器
为了进行多位数的加法,全加器 可以串联起来形成一个 加法器,以处理多个位的二进制数加法。在这种安排中,每个 全加器 的 进位输出 连接到下一个 全加器 的 进位输入。这样,一个 n 位的 加法器 可以通过串联 n 个 全加器 来实现。
上述 加法器 既可以处理 无符号加法(unisnged),也可以处理 有符号加法(int),因为二进制加法与数据类型无关,从 加法器 来说,只是对 0 和 1 进行操作,数据类型是对于二进制的解释方式。
一般而言,对于 有符号加法,加法器还需要考虑 标志位,所以加法器也被扩展为如下结构:
值得一提的是带符号加法器电路是如何输出各个 标志位 的,简单而言,每个 标志位 都可以通过 加法器 电路中的位信息组合得到。
- ZF(zero flag):当 A+B 的结果中的每一位都为 0 时,ZF 为 1,所以 $ZF = !(F_0 F_1 \cdots F_{n-1})$。
- SF(signed flag):A+B 的结果的正负取决于输出结果的最高位,所以 $SF = F_{n-1}$。
- CF(carry flag):
- 对于 加法,进位即最高位,$CF = C_{out}$
- 对于 减法,$CF = ¬C_{out}$
- OF(overflow flag):$OF = C_{n-1} \oplus C_{out}$,
- 如果 $C_{n-1}$ 和 $C_{out}$ 不同,则表示 符号位 的变化导致溢出,即:
- 一个正数加上另一个正数,结果是负数。
- 一个负数加上另一个负数,结果是正数。
- 如果 Cin 和 Cout 相同,则没有溢出发生。
- 如果 $C_{n-1}$ 和 $C_{out}$ 不同,则表示 符号位 的变化导致溢出,即:
减法运算电路
计算机中没有专门的减法电路,因为 补码 的 减法 可以被转换为 加法 操作:
$$[A - B]_{补} = A_补 + (-B)_补$$
以下电路可以实现操作数 A 和操作数 B 的 加减法。
如果是计算 A+B 的话,将 Sub 设置为 0,直接对 A 和 B 进行 加法计算。
如果是计算 A-B 的话,将 Sub 设置为 1,会对 B 进行取反加一,得到 $-B_补 = \overline{B_补} + 1$,然后使用 加法器计算 A + (-B) 即可。
乘法运算电路
二进制乘法 与十进制乘法的手工计算方式类似:
1011 (this is binary for decimal 11)
× 1110 (this is binary for decimal 14)
======
0000 (this is 1011 × 0)
1011 (this is 1011 × 1, shifted one position to the left)
1011 (this is 1011 × 1, shifted two positions to the left)
+ 1011 (this is 1011 × 1, shifted three positions to the left)
=========
10011010 (this is binary for decimal 154)
在计算机中实现 乘法指令可以通过如下几种方式模拟以上过程:
软件算法
如果没有专有硬件支持,可以通过一系列 加法和 位移操作来实现 乘法。编译器将 乘法运算转换为一个循环代码段,在循环代码段中通过比较、加法和移位等指令实现 乘法运算。
// 如何使用加法和位移来实现两个整数的乘法(了解即可)
unsigned int multiply(unsigned int a, unsigned int b) {
unsigned int result = 0; // 结果初始化为 0
while (b > 0) { // 当第二个操作数大于 0 时继续循环
if (b & 1) { // 检查 b 的最低位是否为 1
result += a; // 如果是,将 a 加到结果上
}
a <<= 1; // 将 a 左移一位,相当于 a 乘以 2
b >>= 1; // 将 b 右移一位,去掉已经处理过的最低位
}
return result; // 返回计算的乘积
}
顺序乘法器
通过硬件的方式串行地模拟手工计算的方式,无符号数的 乘法硬件电路 如下图所示(了解即可):
其思路是通过 右移和 加法,每次输出 乘法结果中的一位,由于原理稍复杂,这里不阐述更多细节。
阵列乘法器
阵列乘法器(Array Multiplier)是一种用于执行 乘法运算的硬件电路,它通过布置一系列 全加器和 半加器在一个二维网格或阵列中来实现,它能够并行处理多个位的 乘法和 累加,可以在一个或多个时钟周期内完成 乘法操作。
除法运算电路
二进制 除法在计算机中的实现与我们在十进制中所执行的传统 除法类似。
00111 商 = 0111 = 7
----------
0010 | 00001111 被除数 X = 15 = 1111 = 00001111
0010 除数 Y = 2 = 0010
-----
0011
0010
-----
0011
0010
----
0001 余数 = 0001 = 1
二进制 除法可以被总结为如下步骤:
- 准备:
- 将被除数和除数都转换为二进制形式。
- 写下被除数和除数,类似于十进制除法的长除法形式。
- 除法:
- 从被除数的最高位开始,与除数进行比较。
- 如果被除数当前部分大于或等于除数,则商为 1,否则商为 0。
- 如果商为 1,则将被除数当前部分减去除数,并将差写在下面。
- 将被除数的下一位数字移下来,与差组成新的被除数部分。
- 重复上述步骤,直到被除数的每一位都被处理完毕。
- 余数:
- 最后一次减法运算得到的差即为余数。
- 如果最后一次减法得到的差是 0,则表示整除,没有余数。
简单的 除法电路结构也是通过模拟以上过程实现(了解即可):
在 除法电路 中,在每次迭代中我们将当前 余数 左移一位,并引入 被除数 的下一位,然后执行 余数减去除数 的操作,接下来通过条件判断检测 减法结果 的符号以确定 商 的当前位。
在每次迭代中,我们可以输出 除法结果 中的一位,重复直到处理完所有位后,可以得到 商 和 余数 的结果。
4 - 类型转换
有符号整数和无符号整数
当 有符号整数 和 无符号整数 之间相互转换时,二进制数据 不变,只是改变了变量(或数字)的类型。
首先需要了解 有符号整数(int)和 无符号整数(unsigned)这两个类型:
int:使用 补码 来存储数据。unsigned:所有位,包括最高位,都用于表示数值,表示非负数。
在现在的 64 位计算机中,一般 int 表示 32 位 有符号整数,unsigned 表示 32 位 无符号整数。
由于 int 和 unsigned 在不同体系计算机中行为不同,所以 C 语言库中也自带 int32_t、uint32_t 这种类型来表示特定位数的 有符号 和 无符号 整数,并在各计算中行为一致。
这里为了方便说明,以 8 位 有符号整数 和 无符号整数 为例:
- 8 位 有符号整数
- 表示的范围为
-128 ~ 127 - 最大数为
127,对应的二进制为0111 1111 - 最小数为
-128,对应的二进制为1000 0000
- 表示的范围为
- 8 位 无符号整数
- 表示的范围为
0 ~ 255 - 最大数为
255,对应的二进制为1111 1111 - 最小数为
0,对应的二进制为0000 0000
- 表示的范围为
当 int 和 unsigned 进行 类型转化 时,位模式 不变,可以理解成计算机存储单元中的二进制表示没有变化,只是从程序层面阐述该二进制数据的方式变了,如下图所示:
整形和浮点数
在计算机中,整形(short, int, long)和 浮点数(float, double)之间可以相互转换,但是在转换的过程中可能出现 精度丢失 或 数据溢出。
整形转浮点数
如果一个整数被转换为浮点数,其结果是否会产生 精度丢失 取决于整数的大小是否超过浮点数可以精确表示的整数范围。
但当整数足够大时,转换可能导致 精度损失。
比如对于 float 类型,其尾数为 23 位,它可以精确表示的整数范围为 $(-2^{24}, 2^{24})$,如果一个 int 类型的数字在这个范围内,则将其转换为 float 时 不会丢失精度。
但是如果 int 类型数字超出这个范围且低 8 位不全为 0 的话,则将其转换为 float 会有 精度丢失。
浮点数转整数
当 浮点数 被转换为 整数 时,小数部分会丢失,因为 整数 不能表示小数。
此外,如果 浮点数 超出了 整形 的表示范围,转换可能会 溢出 或产生未定义行为(取决于语言)。
不同长度的类型转换
| 类型 | 最小位数 | 常见位数(32 位系统) | 常见位数(64 位系统) |
|---|---|---|---|
char | 8 位 | 8 位 | 8 位 |
short | 16 位 | 16 位 | 16 位 |
int | 16 位 | 32 位 | 32 位 |
long | 32 位 | 32 位 | 64 位 |
long long | 64 位 | 64 位 | 64 位 |
float | - | 32 位 | 32 位 |
double | - | 64 位 | 64 位 |
总结一下不同长度的 整数 和 浮点数 的 类型转换 规则:
- 整数和整数
- 较小的 整数类型 被转换为较大的 整数类型 时,高位会被自动填充为 0 或 1,不会有数据丢失。
- 较大的 整数类型 被转换为较小的 整数类型 时,高位会被自动截断,可能有数据丢失。
- 浮点数和浮点数
float转为double时,不会有 精度丢失,double转为float时,可能有 精度丢失。
以 short 和 int 的类型转化为例说明:
/* 将 short 转为 int */
short s = 0x1234;
int i = s;
// 高 16 位被填充为 0
// i = 0x00001234
/* 将 int 转为 short */
int i = 0x12345678;
short s = i;
// 高 16 位被截断
// s = 0x5678
类型转换
在编程中,类型转换 指的是将一种数据类型转换为另一种数据类型。主要分为两种:
- 隐式类型转换(Implicit Type Conversion)(也称为 类型提升,Type Promotion)
- 显式类型转换(Explicit Type Conversion)(也称为 强制转换,Type Casting)
隐式类型转换
在 C 语言中,当算术运算、赋值和比较表达式中涉及的多个变量类型不同时,如果没有显式指定类型,编译器会自动进行 类型转换。
/* 隐式类型转换 */
int i = 5;
float f = 2.5;
float result = i + f; // i 被隐式转换为 5.0 然后进行计算
你在一个表达式中混合使用 int 和 float 时,C 语言会自动进行 类型转换 以使得表达式可以正确计算。通常情况下,低精度类型会被提升到高精度类型,即 int 会被转换为 float。
显式类型转换
也可以使用 类型转换运算符 显式地转换一个类型为另一个类型。
/* 显式类型转换 */
int i = 5;
float f = 3.2;
int result = (int)f + i; // f 被显式转换为 3,然后与 i 相加
显式类型转换 会按照你指定的方式进行,在此过程中可能会发生 精度缺失。