文章目录
1. 进制
1.1 概述
进位制是一种记数方式,亦称进位计数法或位值计数法,以有限种数字符号来表示无限的数值。使用的数字符号的数目称为这种进位制的基数或底数。
常见的有:二进制,八进制,十进制,十六进制。
- 二进制:以0和1来组成的数。
- 八进制:以0~7来组成的数。
- 十进制:以0~9来组成的数。十进制是给我们看的,因为容易理解。
- 十六进制:以0~9和A、B、C、D、E、F来组成的数。像内存地址,网卡的MAC地址。
1.2 进制的转换
1.2.1 任意进制转十进制
先来点基础,十进制的构造可以这样,比如1024,可以使用按权展开法构造:权值为10,指数从0开始
虽然看起来没什么软用,但是对于二进制,比如01101,使用按权展开法可以把它转成十进制:权值为2,指数从0开始
其实如果二进制数中有0的在展开时可以省略。类似的,其实八进制,十六进制都可以使用按权展开法转换成十进制,因为它们的权值:2、8、16及转换方法本身就是转10进制的方法。
1.2.2 十进制转二进制
- 方法一:
只需要将以十进制表示的数不断除以2,直到商为0,得出的余数逆着写就可以得到,称为重复相除法。如下图:
假设十进制为:101,则:
类似的,十进制转八进制只要一直除以8,十进制转十六进制只要一直除以16。
- 方法二:(熟练后可以很快求出比较小的数)
这种方法每次十进制数减去一个与十进制相近的以2为底的指数,能减的记为1,不能减的记为0,并记录相减的结果,然后再把刚刚记录的结果重复以上操作,直到整减。
看图吧:
需要记一下:2^0 到 2^10 的值,不难记。当然如果十进制的数很大,不是很j推荐这种方法,因为自己要直到以2为底的指数的值,一般我只记到2^10。也可以使用这种方式来判断十进制转二进制大概有多少位二进制,因为第一次的相减其实就是再求该二进制的最高位,把指数+1就可以得到。
1.2.3 小数点的十进制转换二进制
使用的是重复相乘法,将小数部分乘以2(分数也可以),然后取整数部分,剩下的小数部分继续乘以2,继续取整数部分,剩一直取到小数部分为零为止(或者积为1为止)。如果永远不能为零,就同十进制数的四舍五入一样,按照要求保留多少位小数时,就根据后面一位是0还是1,取舍,如果是零,舍掉,如果是1,向入一位。换句话说就是0舍1入。如下图:
注意:小数点的二进制中的指数是从-1开始的,一直减小。整数的二进制中的指数是从0开始的,一直增大。
1.2.4 二进制转其他进制
- 二进制转八进制:只要把二进制中从右到左每3位,按权展开并相加,不足3位的补0,最后把这些结果组合起来即可。
- 二进制转十六进制:只要把二进制中从右到左每4位,按权展开并相加(如果结果超过9,那么用A、B、C、E、F表达),不足4位的补0,最后把这些结果组合起来即可。
2. 有符号数与无符号数
针对二进制来说可以分为两种:
- 有符号数:有正负值。使用最高二进制位来表示符号,这一位被称为符号位(Sign bit)。人们规定,符号位为 0 表示正数,符号位为 1 表示负数。范围:-2^(n-1) ~ 2^(n-1)-1 (其中n为二进制数的位数)
- 无符号数:无负值。范围:0 ~ 2^(n-1)-1 (其中n为二进制数的位数)
假设二进制以8位为例,有符号数得把最高位作为符号位,那么其余7位来表示二进制数,而无符号数8位都可以表示一个二进制数,所以有符号数可表示的最大值比无符号数可表示的最大值小:
-
有符号数可表示的最大值:01111111 -> 127(最左边,也就是最高位为符号位,所以可表示的范围只有后7位)
-
无符号数可表示的最大值:11111111 -> 255(没有符号位的概念)
对于有符号数可表示的最小值,比较特殊,通过上面的公式,有符号数可表示的最小值为-128,这里有个问题?为什么是-128而不是-127,不应该是11111111(最高位为符号位)???等学了补码和反码在补充!因为其实-128是用补码表示的。
Java中没有无符号数,C++有。
有符号数可以分为正值,原码、反码、补码四种编码实现。
正值:其实就是求出一个数的绝对值的二进制数,不看符号位。比如-3的正值:0011;4的正值:0100。后面要用该知识。
2.1 原码
原码其实就是有符号数:在符号位使用0表示正数、1表示负数。就是为了能够表示负数。
但是可以发现,对于十进制数0,按照原码的表示,可以有00和10,这样就有争议。更重要的是,当两个符号位不同的二进制进行运算时,会出错,例如:1+(-1)=0,在计算机中是使用二进制运算的,假设二进制数有4位,所以变成:0001+1001=1010,换算成十进制为-2,这明显错了。
当然也有另一种运算方式:判断两个操作数绝对值大小,使用绝对值大的数减去绝对值小的数,对于符号值,以绝对值大的为准,但是这样好麻烦啊!!而且计算机规定只会加法操作(因为只有加法器,先不管其他操作怎么处理)。
自从引入原码,那以后给出的二进制转换十进制,最高位究竟是符号位还是数字位?假设题目没有明说,那就是没有符号位的,假设要有说符号位,或者转成原码,那么就肯定有符号位。
像平时的二进制运算,没特殊说明,就直接相加没问题。
用正数的原码进行运算肯定没问题,但用负数的原码进行运算就有问题了,所以为了让原码运算时不出错,并且要消除减法操作,提出了反码。
2.2 反码
反码在原码的基础上提出,定义:(别管怎么来)
根据公式,对于正数的原码跟反码一样,而对于负数,来看看下面的例子,假设有4位二进制数:
[-2]_原 = 1010
[-2]_反 = (2^(4+1)-1)+(-2) = (100000-0001)-(0010) = (011111)-(0010) = (1101)
本来按照上面的计算,-2的反码应该是11101,但是现在我们只有4位二进制数,所以自然舍弃最高位。然后剩下的4位中最高位还是表示符号位。
按照上面的例子(下面:2.3 补码 中得出的结论我先拿上来用),我就先直说了原码和反码的关系:对于正数,它的反码和原码是一样的;对于负数,它的反码等于原码除符号位外其余位取反(即0变1,1变0)。可以多用几个数试试。
现在利用反码来计算:1+(-1)=0,假设有4位二进制数:
[1]_原 = 0001,[-1]_原 = 1001
[1]_反 = 0001,[-1]_反 = 1110
[1]_反+[-1]_反=0001+1110=1111
1111是反码,逆向思想转成原码,得:1000,即用十进制表示:-0。这样运算就正确了,虽然对于0还是有+0和-0的争议。
但真的运算就都正确了吗?再来一例子:5+(-3)=2,假设有4位二进制数:
[5]_原 = 0101,[-3]_原 = 1011
[5]_反 = 0101, [-3]_反 = 1100
[5]_反+[-3]_反=0101+1100=10001
10001有5位,但是目前是使用4位二进制数,所以舍弃最高位,即:0001,转为十进制:1。可以看到跟结果还差1。 再来例子:6+(-2)= 4,假设有4位二进制数:
[6]_原 = 0110, [-2]_原 = 1010
[6]_反 = 0110, [-2]_反 = 1101
[6]_反+[-2]_反=0110+1101=10011
还是跟上面一样,结果舍弃最高位得:0011,转为十进制:3。还是跟结果相差1。有两个例子那么就可以得到:反码虽然在运算时没有使用减法,但是再求反码时使用了减法,并且还是不能运算正确,而且对于0还是有歧义,究竟是-0还是+0。
因此,提出了补码。
2.3 补码
补码是按照下面的公式定义的:
可以看到,对于正数,它的补码跟原码一样,对于负数,涉及到了减法操作。
此时利用补码来运算刚刚的例子:1+(-1)=0,假设二进制数有8位,现在使用补码来表示:
- 1的补码:00000001。
- -1的补码(根据公式):2^(8+1) + (-1) = 2^9 - 1 = 100000000 - 00000001 = 11111111 。
- 相加:00000001+11111111=100000000。注意结果有8个0,一共9位,首先我们规定二进制数有8位,就是一个可表示二进制数范围(实际可表示的只有7位,第8位为符号位),所以100000000其实进位了,但是目前只能用8位表示,会自动舍去最高位,即00000000,就是我们的最终结果。
通过上面的运算,发现对于0的歧义也解决了,补码就是用00000000来表示0的。
记住,计算机如果要判断符号位开销比较大,所以在补码中干脆把符号位一起运算。
想必还是有很多问号???这样还是有减法操作啊!!!画张表来看看有没有规律,一个数的原码和补码、反码的联系:
解释:
- 正数的原码、补码、反码都是一样的。
- 负数的原码,把原码中除符号位的0换成1,1换成0就变成了反码;而反码和补码又有联系,就相差1,即负数的补码等于反码+1。
- 其实也可以通过原码心算出补码,仔细观察负数的原码和补码有没有什么联系,我就直说了:从原码的右边(低位)开始,找到第一个二进制数:1,然后把第一个1之后(左边,不包括第一个1)的0换成1,1换成0,符号位不变。 计算机不是用该操作。
所以,计算机中二进制的运算是以补码来进行运算(当然也有说:计算机对正数是以原码来进行运算,对负数是以补码来进行运算,其实都可以,但一条句子是不是更方便记住),反码的提出是为了让计算机求出补码。在计算机中很容易实现,因为有:非门,所以计算机的计算是:先求出正值,再全部用非门取反,得出反码,再加1,就得出补码,比如-3的补码:
- -3的正值:0011;
- 全部用非门取反:1100,得出反码;
- 反码+1:1101,所以-3的补码为1101。
我们人工就不必使用减法(公式)去求什么补码,反码,直接套用该规律,而计算机也巧妙地避开减法操作,把减法转换成加法。
关于0的特殊性:
[+0]原 = 00000000, [-0]原 = 10000000
[+0]补 = 00000000, [-0]补 = 00000000
[+0]反 = 00000000, [-0]反 = 11111111
2.3.1 补码的溢出
想到要是两个负数转成补码相加的话,符号位产生进位,如果自然舍去,结果却变成正数,是不是有很多问号啊???
到此,会对上面的进位(自然舍去),进位(自然舍去)是属于正常的运算范围,而还有一种就是溢出,溢出的话是属于不正常的运算范围。
谈谈溢出,加法运算时有以下情况:
- 正数+正数
- 正数+负数(或负数+正数)
- 负数+负数
减法有以下情况:有了补码,用加法来替换
- 正数-正数:对应上面第一种情况,转换为:正数+(-正数)
- 正数-负数(或负数-正数):对应上面第二种情况,转换为:正数+(-负数)
- 负数-负数:对应上面第一种情况,转换为:负数+(-负数)
所以只看加法来判断溢出:使用双高位来判断溢出,即用C1表示符号位是否进位,C2表示数值部分最高位记是否进位,假设有4位二进制数,有以下的情况:
- 当C1为真,C2为假:说明溢出了,因为两个符号位原本为1,进位后变成0(进位后变10,而符号位取后一位),说明错误了,称为负溢出。
- 当C1为真,C2为真:比如5+(-3):转为补码:0101+1101=10010,因为只有4位二进制数,所以舍去最高位,变为:0010。正确结果。
- 当C1为假,C2为假:比如-5+3:转为补码:1011+0011=1110,正确结果。
- 当C1为假,C2为真:说明溢出了,因为两个符号位为0,在C2进位后,符号位变成1,说明错误了,称为正溢出。
还有其他判断方法,比如变形补码。也就是双符号位的形式(把原来单符号位变双)去运算,当运算结果的符号位出现“01”或者“10”时,则表示产生溢出。 很简单,可以试试。
总结:
- 正数和负数之间的相加一定不会溢出。
- 正数和正数相加有可能会溢出,溢出的情况是数值部分向符号位进位,即相加后符号位为1,变成负数,说明溢出,称为正溢出。
- 负数和负数相加有可能会溢出,溢出的情况是符号位进位了,即相加后符号位为1,变成正数,说明溢出,称为负溢出。你说:啊,那正常两个负数相加的符号位都为1,不都进位吗?你可以试试,比如我使用-3+(-2):转为补码:1101+1110=11011,舍去最高位,得:1011。所以负数加负数相加,如果不溢出的话符号位肯定还是1,但是需要自然舍去一位。
而计算机是怎么处理溢出的,参考高级语言的基本数据类型,当两个数相加后超过了基本数据类型能表示的范围时发生了什么?其实还跟下面的原理有点关系,就是使用同余定理。
2.3.2 补码的原理(了解)
补码的原理是利用模运算和同余定理。
模是指一个计量系统的计数范围。如时钟的计量范围是0~11,模 = 12。“模”实质上是计量器产生“溢出”的量,它的值在计量器上表示不出来,计量器上只能表示出模的余数。
模运算:比如 5 Mod 1 = 1,如果按照高级语言的写法:5 % 1 = 1。这里说个技巧:整数要是小于模的,直接输出整数,要是等于模输出0,要是大于模,则输出:整数-模的结果。
同余定理是模运算中的重要定理(要看证明的,点击百度百科):**两个整数除以同一个整数,若得相同余数,则二整数同余。**记作 a ≡ b (mod m),读作 a 与 b 关于模 m 同余。比如:1 mod 4 = 1,5 mod 4 = 1,所以1与5关于模4同余。
找个例子并结合计算机来理解,时钟就是天生的使用同余定理的物品,时钟可以显示112(或者011)的数字,说明模为12,假设现在指向6点,我们要调到4点,那么按照公式:6-2=4,即回调到4点,这里需要用到减法,想想有没有另一种方法把减法替换成加法。
可能想到了,就是让指针前调,即:(6+10)Mod 12 = 4。这里就把减法转变成加法,即可以把减2当成加10来看待,以下是证明:
- 对于正数的求余很简单,按上面模运算我说的技巧,其实你就可以知道对于正数的补码为什么跟原码一样。因为正数求余后还是正数,比如 1 MOD 4 = 1,2 MOD 4 = 2。
- 对于负数的求余需要一条公式:x mod y = x - y ⌊⌋(x / y ),意思是:x减去y乘以(x除以y的商然后向下取整,即取下界)。要理怎么来去百度吧,并且高级语言对负数去模的结果可能不同。
⌊⌋:该符号表示向下取整,即比自己小的最大整数,比如 1.7,那么向下取整后为1
⌈⌉:该符号表示向上取整,即比自己大的最小整数,比如 1.7,那么向上取整后为2
跟二叉树中的floor和ceil一样。
来试试负数求余的公式:
(-2) MOD 4 = -2 - 4 * (⌊⌋(-2/4))= -2 - 4 * (-1) = -2 + 4 = 2
再来一条:
(-3) MOD 2 = -3 - 2 * (⌊⌋(-3/2))= -3 - 2 * (-2) = -3 + 4 = 1
现在回到时钟去,刚刚说减2当成加10来看待,那么来看看
(-2)MOD 12 = -2 - 12 * (⌊⌋(-2/12))= -2 - 12 * (-1)= - 2 + 12 = 10
看到了吧,而:
10 MOD 12 = 10
所以-2和10是同余的或者称为互为补数,所以在12为模的系统中,-2可以看成是+10。其实可以看成是-2加上一个12的周期,得出10。那么把6记为a,-2记为b,12记为mod,则用公式来表达:a - b = a - b + mod = a + (mod - b)。
总结一句话:在有模的系统中,减去一个数可变成加上它的补数。
现在回到计算机来,假设有4位的二进制数,除符号位外,其范围为-8~7,一共有16个数,那么模为2^3=16,能够表示的最大二进制为0111,那么此时加1,得:1000,而1000其实是-8(对于有符号数来说,不懂看下面的对于-128的解释),这也就跟时钟一样,形成一个环。
所以假设当前的数为6,想要变成2的话,那么我们就得减掉4,但是减法计算机不能运算啊,所以可对减4再加上一个16的周期(模),得出12,所以减4和加12其实是一样的。你可以画条水平线,然后把-8~7填上,然后在6的位置加12试试。此时按照8位的表示范围,纳闷12怎么求,其实计算机只是学了思想,并不一定像它那样操作,可看看下面的运算过程:参考:补码原理——负数为什么要用补码表示,补码的推导
# 按以上理论,减一个数等于加上它的补数,所以
6 - 2
# 等价于 其实 16 - 2 不就是上面补码定义的公式求负数给出的 2^n + x 吗?回去看看
6 + (16 - 2) // 算术运算单元将减法转化为加法
# 用二进制表示则为:
0110 + (10000 - 0010)
# 等价于 10000 确实可转为 1 + 1111
0110 + ((1 + 1111) - 0010)
# 等价于
0110 + (1 + (1111 - 0010))
# 等价于 其实下面是求出反码了,计算机是直接到这一步,因为有非门的存在,根据正值直接全部按位取反。可能有人要说了,要是正数怎么办,最高位不是也变了吗?:不对!正数求反码不变啊。
0110 + (1 + 1101) // -2的正值(0010)的反码(1101),然后+1,正是补码的定义
// 其实不说正值,说出-2是由2(0010)的补码(0010)全部包括符号位按位取反(1101)后+1得出的。
# 等价于
0110 + 1110
# 所以从这里可以得到-2的补码
-2 = [1110]补
# 即 `-2` 在计算机中的二进制表示为 `1010`,正是“ -2 的正值 2(0010)的补码(1110)”。这就是补码的推导
# 最后一步 0110 + 1110 等于
10100 而只有4位,舍去最高位得:0100,转为十进制:4,正确!!
对于8位的二进制,-128用补码1000 0000表示的解释:其实是取消了原码和反码对于0的歧义问题,在补码中就直接用1000 0000来表示-128。计算机就固定用1000 0000来表示-128,并且是不能转换的,比如有人要把-128转为原码和补码,这是错误的。这也解释了为什么最小值还会多出一位,因为多出的一位用来消除0的歧义。
(看看计算机自带的计算器)
可能你会问那如何表示128,那么128肯定是要数据能够表示的范围啊!!对于8位表示不了128。我废话了。。
补码原理花了我好长时间,今天终于搞完了,虽然可不知道原理,但是现在复习阶段碰到了就搞搞吧。
2.3.3 小数的补码
定义:
前面在进制转换中已经有说了小数的转换,那么求小数的原码也很简单。
更重要的是,小数的原码,反码,补码怎么求是跟整数的求法一样的。
对于小数的符号位放哪?有些书放在小数点之前,比如:1.0101,0.10,也有额外再开个空间存储符号位,比如:10.0101,00.10。这些都可以。
我直接拿老师的例子:他是额外开空间存储符号位的
3. 定点数
计算机中处理的数据经常带有小数点,而小数点在计算机中有两种表示方式,一种就是定点数,另一种就是浮点数。
定点数:小数点固定在某个位置的数称为定点数。按照位置的不同,可分为两种:
- 定点小数:小数点默认位于数值部分的左边,整数位则用于表示符号位,称为纯小数(相对于二进制来说),但是小数点也是隐含的,即不占位。比如(都是二进制表示):0.001(十进制数为:0.125),1.0111(-0.45,假设保留4位小数点)等。
(网上还有老师都是上图,但是我百度纯小数时,比如百度百科中说:整数部分为零的小数叫做纯小数,其实它表示的是十进制,符号位是’+‘或’-’,比如:0.12,-0.2333等。)
百度百科,一般来说,如果最末位 1,前面各位都为0,则数的绝对值最小,即|x|min= 2^(-n)。 如果各位均为1,则数的绝对值最大,即|x|max=1-2^(-n)。所以定点小数的表示范围是:(其中n为二进制数的位数)
- 定点整数:小数点默认放在 有效数值部分之后,最高位还是符号位,称为纯整数(相对于二进制来说),但是小数点是隐含的,即不占位。比如(最高位为符号位):0001(十进制:1),0010,1011等。而范围就是上面有符号数那个范围:-2^(n-1) ~ 2^(n-1)-1 (其中n为二进制数的位数)
但计算机通常遇到既不是纯整数又不是纯小数。比如:10.2,3.53等。
对于计算机来说,需要把它统一称纯小数或纯整数,怎么搞??数据按比例因子缩小成纯小数或扩大成纯整数再参加运算,结果输出时再按比例折算成实际值。不过有点麻烦,所以提出浮点数。
4. 浮点数
浮点数:是属于有理数中某特定子集的数的数字表示,在计算机中用以近似表示任意某个实数。具体的说,这个实数由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到,这种表示方法类似于基数为10的科学计数法。
科学计数法:
- 1.2345:称为尾数。
- 10:称为基数。
- 8:称为指数。表示的是小数点的真实位置。
4.1 二进制科学计数表示法
而对于浮点数的表示格式跟科学计数法一样,前两个数的称呼也跟上面一样。而指数在这里称为阶码。
- 计算机中保存的格式如上的蓝条,其中对于基数不用存储,因为在计算机中默认就是:2。
- 在浮点数中,尾数的位数越多,有效精度越大。在计算机中尾数用补码表示。对于尾数是有要求的,就是尾数必须是纯小数,如果尾数最高位为1的浮点数称为规格化数。规格化主要是为了提高精度。
- 阶码的位数越多,表示的范围越大。这里的阶码在计算机中用补码表示。
浮点数的例子,都是以二进制表示的:
- 上面的写法都是合法的,就拿第一个来看。一般运算都是需要规格化浮点数。
- 0.0110101:以二进制表示的尾数,必须转成纯小数。小数点之前是符号位。
- 2:基数,在计算机中默认为2。
- 3:以十进制数表示,称指数或阶码。
- 那么在计算机中就是这样存储的(根据上面的蓝条):0 11 0 01101010(假设8位尾数,不足8位在后面补0,不是前面补0,因为是这有小数点的,这里采用原码表示)
- 小数点右移一位阶码-1,小数点左移一位阶码+1。
4.2 IEEE754的表示方法(重要)
对于原先的二进制科学计数法,因为早期不同机器上的默认的阶码位数和尾数长度可能不一样,所以对浮点数的表示有点差异。所以就要统一浮点数的表示,提出IEEE754标准。
- 在IEEE754中,少了阶码的符号位,阶码用移码表示。移码是阶码加上一个偏移值,因为阶码有正负,而IEEE754不保存阶码的符号位,所以使用一个偏移值,来取消阶码的正负之分(其实就是把一个有符号数变成无符号数),因此少了阶码的符号位。
- IEEE754规定偏移值为: (e为阶码位数),比如8位阶码的偏移值为: = 128 - 1 = 127。
- 移码的好处:采用指数的实际值加上固定的偏移值的办法表示浮点数的指数,好处是可以用长度为e个比特的无符号整数来表示所有的指数取值(就不用浪费一位存储阶码符号位),这使得两个浮点数的指数大小的比较更为容易,实际上可以按照字典序比较两个浮点表示的大小。像以前使用补码来表示阶码,要比较大小很难比较。
- 尾数必须是纯小数,如果尾数最高位为1的浮点数称为规格化数。这么说当然还有非规格化的数和一些特殊值。
- double(双精度,64位,符号位1位,阶码11位,尾数52位。),float(单精度,32位,1位符号位,8位阶码位,23位尾数位)使用IEEE754表示的。待讲。
IEEE754根据阶码和尾数还可以再分类。
4.3 IEEE754规格化浮点数
所谓规格化,就是要求尾数最高位为1,这可以通过阶码调整。所以在IEEE754中可以把最高位的1省略掉,因为规格化浮点数默认最高位肯定是1啊。所以有了以下的公式:
- (1+M):因为能控制阶码,所以可以对原先的尾数进行操作,把尾数最左边第一个为1提到小数点左边,而小数点右边就当成是M,比如:0.00101000 ,按照说法,变成:1.01000000 * ,而存入计算中只需要存入:01000000就行。而拿出来时就需要M+1。由此可得规格化浮点数的范围为:1<=1+M<2。(1<=M<2,也有这样的,这里的M指的是原先没有去最高位1的尾数)
- 根据上面,对于规格化的浮点数,单精度的尾数为23位,但实际上为24位,双精度也是隐藏了一位。所以从规格化浮点数转换到十进制时需要注意尾数。
- 上面的尾数的疑问,说要是找不到1怎么办,就是全为0,这是一个特殊值,有特殊处理。
- 规格化后的尾数的最高位:正数的原码和补码最高位都是1,负数的原码最高位为1,负数的补码最高位为0。网上的规格化题目很多都是用原码表示的,那么下面的题就都用原码表示,容易理解。而按网上说的现阶段计算机中的数据都是按补码表示的,那我认为在计算机中的浮点数尾数是补码形式。有错指出。
- 阶码用移码表示。移码=阶码+偏移值。偏移值= (e为阶码位数)。
- 对于阶码进一步解释,按照单精度有8位阶码,因为没有符号位,那么阶码可表示的范围为0~255(十进制表示)。但0(二进制全为0)和255(二进制全为1)是特殊值,那么阶码可表示的范围为1~254。那负数怎么表示?借助偏移值,单精度的偏移值为127,所以阶码实际上表示的范围为:-126~127。
拿个例子:假设把-5.125转成规格化的单精度浮点数。(单精度浮点数为4个字节,IEEE754规定1位为尾数符号位,8位为阶码,23位为尾数,这里用原码表示)
- -5.125转成二进制:-101.001,把符号位和尾数分离,此时S = 1。
- 10.111转成最高位为1的浮点数:1.0001* ,此时M=0001 0000 0000 0000 0000 000。
- 单精度的偏移值: = 128 - 1 = 127。
- 阶码用移码表示:E = 2 + 127 = 129。转成二进制:129 = 10000001。
- 所以按照规格化的格式:尾数符号位 阶码 尾数:1 1000 0001 0001 0000 0000 0000 0000 000。
4.4 IEEE754非规格化浮点数
(我看百度百科写得很好,就直接拿过来这两段)
如果浮点数的指数部分是0,尾数部分非规格化,那么这个浮点数将被称为非规格化的浮点数。在这样情况下,阶码=1-偏移值,尾数就是原先的尾数,即不隐含尾数开头的1。
一般是某个数字相当接近零时才会使用非规格化浮点数来表示,其实可以把表示0归类在非规格化浮点数。(重要)
IEEE 754标准规定:非规约的浮点数的指数偏移值比规格化的浮点数的指数偏移值小1。例如,最小的规格化的单精度浮点数的指数部分编码值为1,指数的实际值为-126;而非规格化的单精度浮点数的指数域编码值为0,对应的指数实际值也是-126而不是-127。实际上非规格化的浮点数仍然是有效可以使用的,只是它们的绝对值已经小于所有的规格化浮点数的绝对值;即所有的非规格化浮点数比规约浮点数更接近0。
为什么要让非规格化浮点数的指数偏移值比规格化浮点数的指数偏移值小1?为了补偿非规格化数的尾数没有隐含开头1,其实非规格化浮点数经常性的与规格化浮点数互相转换,所以这是为了让非规格化浮点数能够平滑转换到规格化浮点数,减少误差。虽然可能还是有误差。
来看看《深入了解计算机系统》的浮点数中讲的:解释为什么阶码要用1-偏移值表示。
4.5 IEEE754特殊值浮点数
-
如果指数是0并且尾数的小数部分是0,这个数±0(和符号位相关)。假设为单精度:
- 0 00000000 00000000000000000000000:+0
- 1 00000000 00000000000000000000000:-0
- 根据IEEE754,这两个值是不同的。
-
如果指数= (转为二进制就是阶码全为1)并且尾数的小数部分是0,这个数是±∞(同样和符号位相关)。假设为单精度:
- 0 11111111 00000000000000000000000:+∞
- 0 11111111 00000000000000000000000:-∞
-
如果指数= (转为二进制就是阶码全为1)并且尾数的小数部分非0,这个数表示为不是一个数(NaN)。比如:Math.sqrt(-1)。假设位单精度:
-
0 11111111 00000000000000000000001:NaN
-
…
-
0 11111111 11111111111111111111111:NaN
-
一共有: 个表示NaN。
-
双精度的也可以求,根据尾数位数,所以一共有 个表示NaN。
-
一些运算结果不能是实数或者无穷,就会返回NaN,比如:Math.sqrt(-1)。
-
4.6 浮点数的表示范围
假设阶码位数为m位,尾数位数为n位,先按非零浮点数来说,它可以表示上面三类数。跟高级语言联系,按照java中的浮点数最小值是非规格化数,最大值是规格化数。
对于正数的阶码:用移码表示。
正数阶码最大值(规格化的阶码的最大值,减去偏移值):
正数阶码最小值(非规格化数的阶码的最小值) :
对于正数的尾数:
尾数最大值(规格化数,比如:1.1111… 后都是1,规格化尾数小于2,最大值离2有2^{-n}的距离):
尾数最小值(非规格化数,距离0的距离就是2^{-n}):
根据上面尾数的范围和阶码的范围,可得出浮点数的表示范围:其中有三块是不能表示的(上溢,下溢),按照Java,对于浮点数,如果产生下溢,一律按0.0处理,如果右上溢则按正无穷处理,如果左上溢则按负无穷处理。
- 关于溢出:假设八位定点数,不考虑阶码,那么它能够表示的最小的数是0.00000001,更高的精度就无法表示了,比如:0.00000000000000000000001,这个就是下溢,那么浮点数同样存在这个问题。
- 那么假设浮点数能够表示的绝对值最小的数是N,那么N/2则没办法表示了,因而为了表示N/2,就会产生精度丢失,这就是浮点数下溢的情况。
上面浮点数的表示范围可能有时会不同,不要照搬,看阶码用什么表示,还有表示的范围是否有包含特殊值,是否还分规格化和非规格化等等。
4.7 浮点数的舍入
任何有效数上的运算结果,通常都存放在较长的寄存器中,当结果被放回浮点格式时,必须将多出来的比特丢弃。IEEE754定义了浮点数四种舍入格式:
- 舍入到最接近(默认使用这个):如果尾数超过规定的位数,则按0舍1入的方式截取到规定的位数。比如8位尾数,此时尾数超过8位,那么在第9位判断是0还是1,如果是0,则截取前8位,如果是1,则截取前8位并对其加1。这就是我们十进制说的四舍五入。
- 朝+∞方向舍入:会将结果朝正无限大的方向舍入。
- 朝-∞方向舍入:会将结果朝负无限大的方向舍入。
- 朝0方向舍入:会将结果朝0的方向舍入。
4.7 浮点数的运算
这里是有符号位的。如果从计算机取出数据时,
需要5步:对阶-》尾数求和-》尾数规格化-》舍入-》溢出判断
- 对阶:小阶看齐大阶,一般是小数点左移。就是比较小的阶码转成跟另一个阶码一样大。这样就可以运算。
- 尾数求和:利用补码来运算。而因为有补码,所以可以把减法变加法,所以都是求和。
- 尾数规格化:如果不是下面的格式则需要变成下面的格式,因为这是有符号位的,小数点的位置不能动,所以后面的移动说成尾数整体。一般尾数整体左移。如果双符号位不一致下需要对符号位和尾数一起右移变成相同的符号位(01.xx->00.1xx,10.xx->11.0xx),如果是右移的话需要有舍入操作。所以会有点误差。但是只要是移动就需要改变阶码,整体左移一位阶码-1,整体右移一位阶码+1。不理解就用十进制理解。
- 如果使用补码,对于正数,那么它的补码的尾数规格化的形式为:0.1xxxxxx
- 如果使用补码,对于负数,那么它的补码的尾数规格化的形式为:1.0xxxxxx
- 如果使用变形补码(双符号位),对于正数,尾数规格化的形式为:00.1xxxxxx
- 如果使用变形补码(双符号位),对于负数,尾数规格化的形式为:11.0xxxxxx
- 舍入:0舍1入。
- 溢出判断:可以使用多种方式判断,这里使用变形补码。跟定点数不同。但浮点数的尾数双符号位不一致时不算溢出,因为可以通过右移符号位使得相同。而真正判断溢出是用阶码的双符号位来判断的,但阶码的双符号位不一致时,才算浮点数溢出。
例1:x = 0.1101 * , y = (-0.1010) * ,求x+y。(都是用二进制表示的,假设阶码和尾数都是4位)
- 对阶:x的阶码比y的阶码小,所以x向y看齐,尾数整体右移动,得:x = 0.001101 * ,截取前4位:0.0011 *
- 尾数求和:使用双补码,为了判断溢出:
- x[补] = 00.0011,y[补] = 11.0110
- x[补] + y[补] = 11.1001
- 尾数规格化:发现满足负数的变形补码的规格化,则变:
- 11.1001 -》11.0010(尾数整体左移一位,符号位不动,这里左移直接把尾数最高位舍掉了,后面补0)
- 那么阶码此时需要改变,尾数整体左移一位,则阶码-1,变成:10
- 溢出判断:阶码双符号位是00,没有溢出。
例2:x = 0.11010011 * , y =0.11101110 * ,求x+y。(都是用二进制表示的,假设阶码是4位,尾数是8位)
- 对阶:需要改变y:0.011101110 * ,截取8位:0.01110111 *
- 尾数求和:
- 这里x和y都是正的,所以补码跟原码一样。
- x[补] + y[补] = 01.01001010
- 规格化:01.01001010 -》00.101001010
- 舍入:最后一位为0,所以不需要进位,得:00.10100101,阶码+1:1110
- 溢出判断:阶码双符号位是00,没有溢出。
4.8 常见的浮点数类型
都是以IEEE754为标准的。
有效数字是指在一个数中,从该数的第一个非零数字起,直到末尾数字止的数字称为有效数字。
- 单精度浮点数:使用4个字节(32位)来表达的浮点数(float),包括符号位1位,阶码8位,尾数23位。
- 精度主要取决于尾数,十进制的双精度浮点数最多有8位有效数字。
- 双精度浮点数:使用8个字节(64位)来表达的浮点数(double),包括符号位1位,阶码11位,尾数52位。
- 十进制的双精度浮点数最多能表示18位有效数字。
我们以单精度浮点数为例,跟高级语言联系起来,我用Java的float联系。Java的float的组成跟上面一样。而我们知道float中有规格化浮点数、非规格化浮点数和特殊值三种。
public final class Float extends Number implements Comparable<Float> {
/**
* 表示正无穷,但输出:Infinity
*/
public static final float POSITIVE_INFINITY = 1.0f / 0.0f;
/**
* 表示负无穷,但输出:-Infinity
*/
public static final float NEGATIVE_INFINITY = -1.0f / 0.0f;
/**
* 表示为不是一个数
* NaN规定用0x7fc00000来表示
*/
public static final float NaN = 0.0f / 0.0f;
/**
* 正数浮点数的最大规格化数
* (1.11111111111111111111111)×2^127 ≈ (2 - 2^(-23))×2^127 ≈ 3.402823e+38f
*/
public static final float MAX_VALUE = 0x1.fffffeP+127f; // 3.4028235e+38f
/**
* 正数浮点数的最小规格化数
* (1.0)×2^(-126) ≈ 1.17549435E-38f
*/
public static final float MIN_NORMAL = 0x1.0p-126f; // 1.17549435E-38f
/**
* 正数浮点数的最小非规格化数,非规格化浮点数的指数为0,按照公式:阶码=1-偏移值,得:阶码=1 - *(2^(8-1)-1) = -126,而非规格化浮点数最小值是距离0最近的,那么尾数为00...01(22个0,一共23 * 位)= 2^{-23},所以正数的非规格化浮点数最小值 = 2^{-23} * 2^{-126} = 2^{-149} ≈
* 1.4e-45f
*/
public static final float MIN_VALUE = 0x0.000002P-126f; // 1.4e-45f
/**
* 最大实际指数
*/
public static final int MAX_EXPONENT = 127;
/**
* 最小实际指数
*/
public static final int MIN_EXPONENT = -126;
/**
* 位
*/
public static final int SIZE = 32;
/**
* 字节
*/
public static final int BYTES = SIZE / Byte.SIZE;
/*
* 可以接收 float,double,String
*/
public Float(float value) {
this.value = value;
}
public Float(double value) {
this.value = (float)value;
}
public Float(String s) throws NumberFormatException {
valueparseFloat(s);
}
/*
*native关键字:方法对应的实习并不在当前文件
* 方法作用:将一个浮点数转成二进制,然后转成成在内存中的表示形式(这里是float,那么1位符号位,8位阶码,23位尾数),再输出时转成十进制输出。
*
*/
public static native int floatToRawIntBits(float value);
/*
* 功能跟上面方法一样,但是这里做了NaN检查
* 在浮点数比较时(equals)会使用到这个方法,如果浮点数相等那么转换的二进制一定相等。
* 我们也可以用它来把一个浮点数转成二进制数,因为它是先转成十进制数,那么需要借助Integer.toBinaryString()方法,把十进制转成二进制,如果输出的二进制不满32位则前面补0,那么对于float,第1位是符号位,8位阶码,23位尾数。
* 如果是double型也一样,但使用Long.toBinaryString()。
*/
public static int floatToIntBits(float value) {
int result = floatToRawIntBits(value);
// Check for NaN based on values of bit fields, maximum
// exponent and nonzero significand.
if ( ((result & FloatConsts.EXP_BIT_MASK) ==
FloatConsts.EXP_BIT_MASK) &&
(result & FloatConsts.SIGNIF_BIT_MASK) != 0)
result = 0x7fc00000;
return result;
}
/*其他的源码,比如parseFloat方法的源码前部分看得懂,但后部分看不懂,以后再补*/
}
最后,学习了浮点数,如果这样完事了就学了没用啊,那么在Java中试试:0.1+0.2的结果是什么?我的结果是:0.30000000000000004。意不意外!!按照上面学的,我们自己来模仿计算机运算一下:注意,计算机如果没有在浮点数后加上f,那默认就得double类型的,这应该不用我废话了。
- 计算机会转为二进制,我使用重复相乘法来转,会发现乘不尽。过程我就不写了,直接得出结果:
0.1 转: 0.00011001100110011...(后面无限循环0011)
0.2 转: 0.0011001100110011...(后面无限循环0011)
发现0.1和0.2转成二进制是一个无限循环的二进制小数,这也说明double并没有能够准确表达0.1和0.2的浮点数,只能找到一个无限逼近0.1和0.2的浮点数。这需要跟对阶和0舍1入联系。
- 那0.1和0.2是不是规格化浮点数?因为要截取尾数位,使用语句来判断:
System.out.println(Double.MIN_NORMAL > 0.1);
output:false
说明0.1和0.2是规格化数,是规格化浮点数,所以尾数有隐藏位
- 规范化表示:(小数点右移一位阶码-1,小数点左移一位阶码+1,这跟上面的浮点数运算说的要区别好,上面是尾数整体,而且有双符号,小数点的位置不能变。)
0.1:0.00011001100110011...(后面无限循环0011)
二进制科学计数法:(-1)^0 * 1.1001100110011001100110011001100110011001100110011010 * 2^(-4)
解释:尾数截取了52个,第53个是1,按照0舍1入,需要对尾数加1
0.2:0.0011001100110011...(后面无限循环0011)
二进制科学计数法:(-1)^0 * 1.1001100110011001100110011001100110011001100110011010 * 2^(-3)
解释:尾数截取了52个,第53个是1,按照0舍1入,需要对尾数加1
- 对阶:小阶看齐大阶,所以需要改变0.1的阶码
0.1100110011001100110011001100110011001100110011001101 * 2^(-3)
后面舍去一位0
- 相加
0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101
+ 1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
----------------------------------------------------------------------
10.0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111
- 对结果规格化:
10.0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111:
(-1)^0 * 1.0011001100110011001100110011001100110011001100110100 * 2^(-2)
解释:尾数截取了52个,第53个是1,按照0舍1入,需要对尾数加1
- 转为十进制:
0.010011001100110011001100110011001100110011001100110100:0.30000000000000004
这里就不使用双符号位了,可以知道不会溢出。所以答案就是:以前学过有限小数和无限小数,浮点数在运算时转成二进制时,也是有这样的概念,所以某些数并不能精确的表示,而且可能经过了0舍1入和对阶,所以某些浮点数只能找到无限接近于它的数。
那0.1f+0.2f怎么不会?再试试:
- 是否是规格化数?
System.out.println(Float.MIN_NORMAL > 0.1);
output:false
说明0.1和0.2是规格化数,是规格化浮点数,所以尾数有隐藏位
- 二进制科学计数法
0.1:0.00011001100110011...(后面无限循环0011)
二进制科学计数法:(-1)^0 * 1.10011001100110011001101 * 2^(-4)
解释:尾数截取了23个,第24个是1,按照0舍1入,需要对尾数加1
0.2:0.0011001100110011...(后面无限循环0011)
二进制科学计数法:(-1)^0 * 1.10011001100110011001101 * 2^(-3)
解释:尾数截取了52个,第53个是1,按照0舍1入,需要对尾数加1
- 对阶:修改0.1
(-1)^0 * 0.11001100110011001100111 * 2^(-3)
舍去最后一位,最后一位是1,需要对尾数加1
- 相加:
0.11001100110011001100111
+ 1.10011001100110011001101
-----------------------------
10.01100110011001100110100
- 对结果规格化:
(-1)^0 * 1.00110011001100110011010 * 2^(-2)
- 转成十进制:
0.0100110011001100110011010:0.30000001192092896
这里的结果就疑问了?为什么结果跟java输出的结果不同,你可以试试下面这条语句:
System.out.printf("%.30f\n", (0.1f+0.2f));
它输出的结果:0.300000011920928960000000000000000000000000000000000000000
你看这不是跟我们计算的一样吗,只不过对于float来说,它最多能表示8位有效数字,即0.3000000,而3后面的0是不显示的,所以就变成了输出0.3
浮点数的运算造成的误差其中的原理都是一样的,而且还要看是float和double类型。
自从了解了上面,那我自己就突然跳出一个想法,那为什么打印0.1结果还是输出0.1呢?我也很纳闷,卡了2天,找了资料,看了源码,提到跟Double/Float的toString有关,主要是toString调用的FloatingDecimal.toJavaFormatString(f),对于现阶段的我难以理解,这个问题十分底层。我给个链接:点我跳转,如果看得懂可以在评论区给我留言,谢谢。
目前结合我自己的理解来解释为什么在java打印0.1结果还是输出0.1?
- 结合float和double来说,来看看IDEA,我误打误撞,让float和double的0.1格式化,多输出几十个小数位:
System.out.printf("float: %.50f\n", (0.1f));
System.out.printf("double: %.53f\n", (0.1));
- 输出的结果有点意外:
- float后面还有别的数,那么拿上面我们计算出的两种类型的0.1的二进制数:
对于float的0.1:0.000110011001100110011001101
对于double的0.1:0.00011001100110011001100110011001100110011001100110011010
- 我直接拿去转了,如图:
- 好像有点眉目了,double的精度本来就比float高,所以double表示的数比float更加精确,但不代表double能完全表示0.1(这一点我也还是疑问,不可能就我上面的转换那样,应该还涉及到java的设计机制,现阶段只是利用上面学的东西);而对于float为什么还是输出0.1,不应该输出0.10000000149011612吗?因为float最多能表示8位有效位,所以输出0.1000000,而1后面的0是不显示的,所以输出0.1。
你有没有疑问是不是我算错了?那我们来使用java逆推一下:
// 把0.1在内存中的表示形式(1位符号位,11位阶码(移码表示),52位尾数)转成十进制
System.out.println(Double.doubleToRawLongBits(0.1));
// output:4591870180066957722l
// 把输出结果转成二进制:
System.out.println(Long.toBinaryString(4591870180066957722l));
// output:11111110111001100110011001100110011001100110011001100110011010
// 一共才62位,我们需要64位,一定是在前面补0,因为java输出的数会去除前导0(比如00002,输出2),
// 所以:0011111110111001100110011001100110011001100110011001100110011010
// 按照double:1位符号位,11位阶码(移码表示),52位尾数
// 得 0 01111111011 1001100110011001100110011001100110011001100110011010
// 阶码转成十进制:01111111011-》1019,这是移码,而double的偏移值是1023,所以实际的指数为-4
// 尾数需要+1,得:1.1001100110011001100110011001100110011001100110011010
// 所以得:0.00011001100110011001100110011001100110011001100110011010
// 这个二进制数不就是我们上面求的二进制数吗,我拿去转成十进制结果就是0.1
- 其实对于其他语言的浮点数都有这样的坑,因为都是按照IEEE754标准,目前一直存在。
那既然这样,发展这么久,那肯定有解决办法,这些误差主要产生在运算中,所以在java中使用java.math.BigDecimal来做浮点数的运算。
java.math.BigDecimal中的构造函数可以接收double,float,String类型的参数,也可以接收int、long类型,因为BigDecimal的实习是利用BigInteger,但是在BigDecimal加入了浮点数的表示。
你试试看0.1:
BigDecimal bigDecimal1 = new BigDecimal(0.1);
BigDecimal bigDecimal2 = new BigDecimal("0.1");
BigDecimal bigDecimal3 = new BigDecimal(0.1f);
System.out.println(bigDecimal1);
System.out.println(bigDecimal2);
System.out.println(bigDecimal3);
结果居然是这样的:
很疑惑,点开BigDecimal的构造函数看看源码,看到注释有这一句:我拿去翻译了
-
参数类型为double的构造方法的结果有一定的不可预知性。有人可能认为在Java中写入newBigDecimal(0.1)所创建的BigDecimal正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。所以最好使用String传入。
-
BigDecimal中带有加减乘除,得出的结果需要存储或者覆盖给某个值。
-
BigDecimal中带有方法可以把BigDecimal转成double:doubleValue()。
拿上面的0.1+0.2来试试:
BigDecimal bigDecimal1 = new BigDecimal("0.1");
BigDecimal bigDecimal2 = new BigDecimal("0.2");
System.out.println(bigDecimal1.add(bigDecimal2));
System.out.println(bigDecimal1.add(bigDecimal2).doubleValue()); // 可以转成double,结果都是一样的,但是使用doubleValue()可以把结果赋给double类型的变量
对于BigDecimal的其他方法就不一一说了。
4.6 浮点数与定点数对比
- 当定点数与浮点数位数相同时,浮点数表示的范围更大。
- 当浮点数尾数为规格化数时,浮点数的精度更高。
- 浮点数运算包含阶码和尾数,浮点数运算更加复杂。
- 浮点数在数的表示范围、精度、溢出处理、编程等方面均优于定点数。
- 浮点数在数的运算规则、运算速度、硬件成本方面不如定点数。
5. 参考
《深入了解计算机系统》第三版
慕课网咚咚呛老师(很不错)