1 前言
有三种重要的数字表示:
- 无符号(unsigned):仅表示非负数
- 补码(two’s complement):可表示整数
- 浮点数(floating-point):可表示实数
所以产生了两种数字表示
- 整数编码:表示较小的数值范围,但是表示精确。
- 浮点数编码:可表示较大的数值范围,但是表示只是近似的。
2 信息存储
通常计算机使用8位的块(字节),用来表示最小可寻址的存储器单元。虚拟存储器:针对于机器级程序而言,就是一个超大的数组空间。每个字节都有一个数字唯一标识,称为地址,所有可能的地址集合称为虚拟地址空间。
2.1 十六进制表示法
计算机中利用十六进制是为了表示方便,比如一个字节用二进制表示为0000 0000~1111 1111, 而用十六进制表示为0x00~0xFF.
二进制与十六进制相互转换简单,在此不表。
二进制、十六进制转成十进制利用定义即可,反过来可利用短除取余法。
2.2 字
每个计算机都有一个字长,用于表示整数与指针数据的大小。虚拟地址的编码长度由其控制,对于一个字长为w
的计算机,其虚拟地址空间大小为
2
w
−
1
2^w-1
2w−1, 常见的计算机有32位长和64位长。
2.3 数据大小
计算机中有用1、2、4、8个字节表示整数的。
C语言中char用于表示单个字节,可表示单个字符或者整数。一般还支持两种浮点型,float(4字节),double(8字节),具体的类型字节数如下图:
2.4 寻址和字节顺序
对于大多数计算机,多字节的对象是存储在连续的字节序列当中,对象的地址指向的是字节序列中最小字节的地址。
2.5 表示字符串
字符串中的每个字符都是有固定的编码,比如常见的ascii编码,字符串中每个字符的地址都独立地对应于他们固有的编码。
2.6 表示代码
相同的程序在不同的系统中编译出地二进制代码是不同的,所以大部分二进制代码在不同操作系统中是不兼容的。
2.7 布尔代数
介绍了一些简单的位运算,布尔代数和布尔环地概念,离散数学中有,跳过。
2.8 C语言中的位级运算
&(与),|(或),~(非),^(抑或),熟悉,跳过。
2.9 C语言中的逻辑运算
||,&&,!对应于命题逻辑中的与、或、非,熟悉,跳过。
需要注意的是逻辑运算输出是 true or false,位运算输出的是运算后的二进制数。
2.10 C语言中的移位运算
讲了下左移右移和如果移动的位数k
超过字长(w)数的处理方法。
- 左移k’位
X << k
:向左移动k位,空余 k 位补0. - 右移k位
X >> k
:分为逻辑右移与算术右移。逻辑右移和算术右移都是向右移动 k 位,前者空余 k 位补0,后者空余 k 位补1.
当移动的位数k
超过字长(w)数时,一般是对k/w
取余。
注意移位运算同加减乘数一样也是左结合的,运算优先级低于 ± \pm ± 运算。
3. 整数表示
描述两种编码来表示整数,一种是无符号表示非负数,一种是有符号整形,表示正数、负数和0。
3.1 整型数据类型
两个图足以说明:
3.2 无符号数的编码
比较简单,如下所示,
B
2
U
w
B2U_w
B2Uw表示Binary to Unsigned
3.3 补码编码
最常见的有符号数在计算机中的表示方式为补码(two’s complement), 在此定义中最高位表示着负权
−
2
w
−
1
-2^{w-1}
−2w−1,有符号数表示如下,
B
2
T
w
B2T_w
B2Tw表示Binary to Two's complement
:
通过定义可知,补码标识的范围是
[
−
2
w
−
1
,
2
w
−
1
−
1
]
[-2^{w-1},2^{w-1}-1]
[−2w−1,2w−1−1]
C语言对有符号数的表示没有明确规定,但是基本上所有机器用的都是补码。Java语言规定用补码表示有符号整型。
有符号数还有其他的表示方式,比如反码和原码。
反码(one’s complement)表示, 最高有效位的权为
−
2
w
−
1
−
1
-2^{w-1}-1
−2w−1−1:
原码表示(signed-magnitude)表示, 最高有效位代表着符号权。
反码与原码对于数字0都有两种不同的表示,+0
都表示为00000000
. -0
在反码中表示为11111111
, 在源码中表示为10000000
. 所以大部分计算机都采用补码的方式。
3.4 有符号数与无符号数之间的切换
两者相互之间的转换只需记住位模式都是一样的,只不过计算十进制的值的规则不同,可通过相同的位模式来进行转换。
无符号数转补码:
补码转无符号数:
3.5 C语言中的有符号数与无符号数
int, unsigned
代表有符号与无符号。
在printf函数中%d, %u, %x
表示有符号十进制,无符号十进制和十六进制格式输出的数字。
3.6 拓展一个数字的位表示
当少数位向多数位拓展
- 对于无符号来讲,开头加 0 填充。
- 对于补码来讲,开头填充最高有效位的值。
3.7 截断数字
当多数位向少数位截断
截断很好理解,就是保留低位的前k位,丢弃掉高位的w-k
位。
3.8 关于有符号于无符号的建议
注意有符号到无符号的隐式转换,避免这一类错误之一就是不适用无符号整型。就像Java中只支持有符号整型,>>表示算术右移(补最高有效位,即符号位),>>>表示逻辑右移(补0)。
4. 整数运算
4.1 无符号加法
加法运算定义如下:
C语言的加法运算便是如此,判断是否溢出的充要条件是
s
=
x
+
y
,
s
<
x
∣
∣
s
<
y
s=x+y, s<x || s<y
s=x+y,s<x∣∣s<y。
4.2 补码加法
简单来说就是将补码转成无符号,利用无符号进行+
运算,最后将运算结果转为补码形式。如下图:
通过上图分析可知,补码加法分为以下三种情况:
4.3 补码的非
由于负数要多一位,所以非运算如下图:
补码位级的非表示:假设k位是最右边的1的位置,然后将k位左边的位全部取反。如下图所示:
4.4 无符号乘法
很简单,如下图所示:
4.5 补码乘法
运算与无符号乘法相同。
4.6 乘以常数
由于乘法的代价太大,当乘以一个常数时,往往会将其分解为求解与若干个 2 k 2^k 2k的和。又由于乘以 2 k 2^k 2k在计算机运算中可表示为向左移k位。比如 x ∗ 14 x*14 x∗14可写为 x ∗ 2 3 + x ∗ 2 2 + x ∗ 2 1 x*2^3+x*2^2+x*2^1 x∗23+x∗22+x∗21,所以可以将 x x x分别左移3位、2位、1位,在这三个平移后的编码求和。
4.7 除以2的幂
- 对于无符号可进行逻辑右移
- 对于补码需要进行算术右移
但是不能推广到除以任意一个常数。
4.8 关于整数运算的最后思考
整数运算其实就是一种模运算,由于字长的限制,运算有可能会导致溢出。另外补码对于有符号整型是一种比较好的编码方式。另外不论是无符号还是补码在运算上都有完全一样或者类似的位级行为。
5. 浮点数
大多计算机都采用IEEE浮点标准
5.1 二进制小数
十进制的小数表示如下:
同理,二进制的小数表示如下:
众所周知,在有限的位数下,十进制无法精确表示
1
/
3
1/3
1/3, 所以二进制也一样,只能通过不断的逼近去近似那些值。
5.2 IEEE浮点表示
IEEE浮点标准用 V = ( − 1 ) s ∗ M ∗ 2 E V=(-1)^s*M*2^E V=(−1)s∗M∗2E的形式来表示一个数。
- 符号(sign):s决定这个数是正数还是负数。
- 尾数(significant):M是一个二进制小数 [ 1 , 2 − ϵ ] [1, 2-\epsilon] [1,2−ϵ]或 [ 0 , 1 − ϵ ] [0, 1-\epsilon] [0,1−ϵ]
- 阶码(exponent):E的作用是对浮点数加权,这个权重是 2 的 E 次幂。(E 可能是负数)
浮点型又分为单精度与双精度类型:
根据exp的不同被编码的值分为三种情况,最后一种情况有两个变种。具体如下图所示:
以单精度为例:
- 规格化,没得讲,就是正常的表示。偏置bias是127. 阶码E= e - bias。e是无符号整数(排除全0和全1的情况),所以阶码指数的表示范围是[-126, 127]. frac与尾数M的关系是M = frac + 1. frac字段表示的数是 0. f n − 1 . . . f 1 f 0 0.f_{n-1}...f_1f_0 0.fn−1...f1f0, 所以尾数M表示的是 1. f n − 1 . . . f 1 f 0 1.f_{n-1}...f_1f_0 1.fn−1...f1f0.
- 非规格化,当exp全为0时。阶码 E = 1 − b i a s E=1-bias E=1−bias, 尾数 M = f r a c M=frac M=frac.
- 特殊值,当exp全为1时。当小数域全为0时表示无穷,s为0时表示正无穷,s为1时表示负无穷;当小数域不全为0时表示“NaN”,表示“不是一个数”,比如 − 1 \sqrt{-1} −1.
5.3 数字示例
上图比较好的说明了浮点型的数字表示。假设数字由8位组成, 阶码占4位,小数域占3位。
5.4 舍入
因为表示方法,有些数字只能近似的表示,所以需要制定舍入(近似)规则。IEEE格式定义了四种舍入规则:向偶数舍入、向0舍入、向上舍入和向下舍入,如下图所示:
5.5 浮点运算
加法和乘法运算后需要进行舍入操作。所以加法和乘法都不具备结合律,乘法还不具备分配律。
比如
3.14 + 1 e 10 ) − 1 e 10 = 0 , 3.14 + ( 1 e 10 − 1 e 10 ) = 3.14 ; 3.14+1e10)-1e10=0, 3.14+(1e10-1e10)=3.14; 3.14+1e10)−1e10=0,3.14+(1e10−1e10)=3.14;
( 1 e 20 ∗ 1 e 20 ) ∗ 1 e − 20 = + ∞ , 1 e 20 ∗ ( 1 e 20 ∗ 1 e − 20 ) = 1 e 20 ; (1e20*1e20)*1e-20=+\infty ,1e20*(1e20*1e-20)=1e20; (1e20∗1e20)∗1e−20=+∞,1e20∗(1e20∗1e−20)=1e20;
1 e 20 ∗ ( 1 e 20 − 1 e 20 ) = 0 , 1 e 20 ∗ 1 e 20 − 1 e 20 ∗ 1 e 20 = N a N ; 1e20*(1e20-1e20)=0,1e20*1e20-1e20*1e20=NaN; 1e20∗(1e20−1e20)=0,1e20∗1e20−1e20∗1e20=NaN;
5.6 C语言中的浮点数
所有的C语言都提供了float和double这两个类型,较新语言中还可以支持long double类型。在应用float和double时,需要十分小心,因为他表示的仅仅是近似的值,而且在运算中会进行舍入。