# 数据的表示和运算
## 数制和编码
- 进位计数制及其数据之间的相互转换
- 定点数的编码表示
## 运算方法和运算电路
- 基本运算部件
- 加/减运算
- 乘/除运算
## 整数的表示和运算
- 无符号整数
- 带符号整数
## 浮点数的表示和运算
- 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编码在某些应用中是很有用的,特别是在需要与十进制界面进行交互的地方,如数字显示或某些早期的计算机系统。尽管它不如纯二进制编码效率高,但它简化了与十进制数据的转换过程。
整数的表示
原码
原码(Sign-Magnitude)是一种简单的方式来表示二进制中的有符号整数。在这种表示法中,数字的最高位(最左边的位)用于表示符号,其余的位表示数字的大小。通常,符号位为0表示正数,而1表示负数。
表示方法
原码的表示方法:
- 正数的原码:最高位为0,其余位表示这个数的绝对值的二进制形式。
- 负数的原码:最高位为1,其余位表示这个数的绝对值的二进制形式。
- 零的原码:符号位可以是0(表示+0)或1(表示-0),但在实际应用中,通常只使用一个零,即符号位为0。
举例(以8位二进制数为例):
+5
的原码是:00000101
-5
的原码是:10000101
原码的缺点
- 存在正零和负零的表示。
- 在进行算术运算时,正数和负数需要不同的处理,这使得硬件设计变得复杂。
由于上述的缺点,原码并不是计算机中最常用的表示法。在现代计算机中,补码是更常用的方式来表示有符号整数,因为它简化了算术运算的处理,并且没有+0和-0的冗余表示。
补码
补码(Two’s complement)是计算机科学中用于表示整数的一种方法,特别是在现代计算机的算术和逻辑操作中。使用补码可以使得加法、减法和负数的表示变得简单和统一。
表示方法
原码的表示方法:
- 正数的原码:最高位为0,其余位表示这个数的绝对值的二进制形式。
- 负数的原码:最高位为1,其余位表示这个数的绝对值的二进制形式。
- 零的原码:符号位可以是0(表示+0)或1(表示-0),但在实际应用中,通常只使用一个零,即符号位为0。
以8位二进制数为例:
- 对于正数,例如 +5,其二进制表示为 00000101,补码也是 00000101。
- 对于负数,例如 -5,首先写出5的二进制表示:00000101。
- 按位取反得到:11111010
- 加1得到:11111011
- 所以,-5的补码是 11111011。
补码的计算方式
以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$
为什么要使用补码
补码的设计使得负数的加法变得简单。当使用补码表示时,可以直接将两个数相加,即使其中一个数是负数,而不需要进行任何特殊处理。这简化了计算机硬件的设计。
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$
IEEE Floating-Point
上述浮点数存储方式的缺点在于如果要表示比较大的数,就需要比较多的二进制位数,比如对于$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$,使用原码表示,其中 $E = exp - Bias$,其中 $Bias = 2^{k-1} - 1$
- 对于单精度浮点数,$k = 8$,$Bias = 2^{8-1} - 1 = 127$,$1 \le exp \le 254$,$-126 \le M \le 127$
- 对于双精度浮点数,$k = 11$,$Bias = 2^{11-1} - 1 = 1023$, $1 \le exp \le 1022$,$-1022 \le M \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$
注意IEEE浮点数表示分为 Normalized Values(正常值) 和 Denormalized Values(非正常值)以及特殊值,上述的计算方式只适用于 Normalized Values, Normalized Values 的 exp 不能为全0或全1,所以上文中 exp 的范围要排除 0 和 最大值的情况。
下图是浮点数各种类型的图示(以单精度浮点数为例):
视频讲解
3 - 运算电路
加法运算电路
最基本的加法单元是 半加器(Half Adder)。它有两个输入,一个是加数,一个是被加数,并有两个输出,一个是和,一个是进位。
全加器(Full Adder)是半加器的扩展,它加上了前一位的进位作为第三个输入,并有两个输出,一个是和,一个是进位。全加器可以级联,用于多位加法。
串联全加器:构成加法器
为了进行多位数的加法,全加器可以串联起来形成一个串行进位加法器(ripple carry adder)。在这种安排中,每个全加器的进位输出连接到下一个全加器的进位输入。这样,一个 n 位的加法器可以通过串联 n 个全加器来实现。
减法运算电路
二进制中的减法可以通过加法来实现。减去一个数等同于加上它的补码。为了得到一个数的补码,你首先得到它的反码(每一位都取反)然后再加1。所以,一个简单的减法器可以通过使用加法器并对第二个输入(被减数)进行补码转换来构建。
乘法运算电路
二进制乘法与十进制乘法的手工计算方式类似:
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)
在计算机中实现乘法指令可以通过如下几种方式模拟以上过程:
1. 软件算法
如果没有专有硬件支持,可以通过一系列加法和位移操作来实现乘法。编译器将乘法运算转换为一个循环代码段,在循环代码段中通过比较、加法和移位等指令实现乘法运算。
// 如何使用加法和位移来实现两个整数的乘法(了解即可)
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; // 返回计算的乘积
}
2. 顺序乘法器
通过硬件的方式实现以上的软件算法,这是最基础的实现方式,类似于我们在纸上做乘法那样,一步一步进行。
3. 阵列乘法器
阵列乘法器(Array Multiplier)是一种用于执行乘法运算的硬件电路,它通过布置一系列全加器和半加器在一个二维网格或阵列中来实现,它能够并行处理多个位的乘法和累加,可以在一个或多个时钟周期内完成乘法操作。
除法运算电路
二进制除法在计算机中的实现与我们在十进制中所执行的传统长除法类似。与二进制乘法一样,我们将使用基本的二进制操作,如移位和减法。
商(Quotient)
0110
=======
10 | 1101
- 10 (这是从 `110` 中减去的,因为它大于或等于 10)
--------
100
- 10 (这是从 `100` 中减去的,因为它大于或等于 10)
--------
10
- 10 (这是从 `10` 中减去的,因为它大于或等于 10)
--------
0 (这是余数 Remainder)
4 - 类型转换
有符号整数和无符号整数
这里需要了解有符号整数(int)和无符号整数(unsigned)这两个类型:
int
:使用补码来存储数据。unsigned
:所有位,包括最高位,都用于表示数值,表示非负数。
以8位有符号整数和无符号整数说明一下
- 8 位有符号整数
- 表示的范围为
-128 ~ 127
- 最大数为
127
,对应的二进制为0111 1111
- 最小数为
-128
,对应的二进制为1000 0000
- 表示的范围为
- 8 位无符号整数
- 表示的范围为
0 ~ 255
- 最大数为
255
,对应的二进制为1111 1111
- 最小数为
0
,对应的二进制为0000 0000
- 表示的范围为
当int
和unsigned
进行类型转化时,位模式不变,可以理解成计算机存储单元中的二进制表示没有变化,只是从程序层面阐述该二进制数据的方式变了。
当我们将8位的有符号整数-128
转化为无符号整数时,其值变成了128
。
整形和浮点数
- 从
float
到int
: 将浮点数转换为整数时,小数部分会被舍去。例如,将 3.9 转换为整数将得到 3。 - 从
int
到float
: 将整数转换为浮点数时,结果会是一个等值的浮点数。例如,将整数 4 转换为浮点数将得到 4.0。
不同长度的类型转换
类型 | 最小位数 | 常见位数(32位系统) | 常见位数(64位系统) |
---|---|---|---|
signed char | 8位 | 8位 | 8位 |
short | 16位 | 16位 | 16位 |
int | 16位 | 32位 | 32位 |
long | 32位 | 32位 | 64位 |
long long | 64位 | 64位 | 64位 |
- 较小的整数类型被转换为较大的整数类型时,高位会被自动填充为0
- 较大的整数类型被转换为较小的整数类型时,高位会被自动截断
以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
隐式和显式类型转换
- 隐式类型转换
你在一个表达式中混合使用 int
和 float
时,C语言会自动进行类型转换以使得表达式可以正确计算。通常情况下,低精度类型会被提升到高精度类型,即 int
会被转换为 float
。
- 显式类型转换
也可以使用类型转换运算符显式地转换一个类型为另一个类型。
/* 隐式类型转换 */
int i = 5;
float f = 2.5;
float result = i + f; // i 被隐式转换为5.0然后进行计算
/* 显式类型转换 */
int i = 5;
float f = 3.2;
int result = (int)f + i; // f 被显式转换为3,然后与i相加