# 数据的表示和运算
## 数制和编码
- 进位计数制及其数据之间的相互转换
- 定点数的编码表示
## 运算方法和运算电路
- 基本运算部件
- 加/减运算
- 乘/除运算
## 整数的表示和运算
- 无符号整数
- 带符号整数
## 浮点数的表示和运算
- 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编码在某些应用中是很有用的,特别是在需要与十进制界面进行交互的地方,如数字显示或某些早期的计算机系统。尽管它不如纯二进制编码效率高,但它简化了与十进制数据的转换过程。
整数的表示
非负数
在程序中使用 unsigned
定义的变量就是非负数表示。
表示方式
在非负数中,数字中的第 $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}$$
二进制 → 十进制
第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 位非负数。
十进制 → 二进制
将十进制转化为二进制 unsigned 的方法就是使用 $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
定义的整形变量就是使用补码(Two’s complement)表示的数字。
表示方式
整形(signed)与非负数(unsigned)的表示方式基本相同,除了最高位。对于一个 $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}$$
二进制 → 十进制
以 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。
另一种就是先计算原码,再将原码转化为补码,具体步骤如下:
- 确定数值的正负性:
- 使用“除2取余法”将 十进制数的绝对值 转换为二进制数。
- 如果十进制数为正数,则其补码与原码相同。
- 如果十进制数为负数,将原码进行 取反加一 得到补码。
比如通过第二种方式将 -73
转为补码:
1. -74 是负数,其绝对值为 73。
2. 除2取余法得到结果为 0100 1001。
3. 取反: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位二进制数为例):
+5
的原码是:00000101
-5
的原码是:10000101
缺点
- 存在正零和负零的表示。
- 在进行算术运算时,正数和负数需要不同的处理,这使得硬件设计变得复杂。
由于上述的缺点,原码并不是计算机中最常用的表示法。在现代计算机中,补码是更常用的方式来表示有符号整数,因为它简化了算术运算的处理,并且没有+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$,使用原码表示,其中 $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$
总结单精度和双精度浮点数表示如下:
类型 | 符号位 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
字面值转二进制
前文已经提到如果我们有浮点数的 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}$
- 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
进行类型转化时,位模式不变,可以理解成计算机存储单元中的二进制表示没有变化,只是从程序层面阐述该二进制数据的方式变了。
当我们将 8 位的有符号整数 -128 转化为无符号整数时,其值变成了 128。
整形和浮点数
在计算机中,整形(float, double)和浮点数(int, long)之间可以相互转换,但是在转换的过程中可能出现 精度丢失 或 数据溢出。
整形转浮点数
如果一个整数被转换为浮点数,其结果是否会产生精度丢失取决于整数的大小是否超过浮点数可以精确表示的整数范围。 但当整数足够大时,转换可能导致精度损失。
比如对于 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 相加
显式类型转换会按照你指定的方式进行,在此过程中可能会发生精度缺失。