很多人都会认为,小数就是浮点数。但其实非也。
小数只是一种实数的一种特殊表现形式,所有分数都可以用小数来表示。
而浮点数,是计算机领域的一个术语,浮点数代表着目前计算机表示小数的一方式。
我们都知道计算机表示特定的数据类型长度是固定的。
比如在java语言里,小数的表示,float是4字节,double是8字节。
那么这些固定长度的二进制位是如何表示小数的呢?
最直观的表示办法就是:固定的整数部分位数和固定的小数部分位数。比如以float为例,我们假设取前8位表示整数部分,后24位表示小数部分。则1.2用该方法表示如下:
00000001 00000000 00000000 00000000 00000010
以上这种表示小数的方法我们称之为:定点表示法,即小数点的位置是固定的(这里固定在第24位之前)。
但是这种定点表示法有一个很大的问题,就是表示数的范围很有限。假设我现在要表示:256.1
那么因为整数部分固定只有8位,将无法表示256,会出现溢出。
于是乎聪明的计算机科学家想到了另一种办法:科学计数法。我们知道10进制下的科学计数法可以将一个数表示成: 1.xxx * 10^n 。
依葫芦画瓢,那么2进制的科学计数法应该长这样:1.xxx * 2^n
那么我们在存储小数的时候,可以用一部分存储指数:n,一部分存储小数:xxx 即可。
而这种表示的方式下,其实小数点没有固定的位置,既小数点是浮动的。所以我们也就称这种存储方式下的数字为浮点数。
IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。
这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number)),一些特殊数值(无穷(Inf)与非数值(NaN)),以及这些数值的“浮点数运算符”;
它也指明了四种数值舍入规则和五种例外状况(包括例外发生的时机与处理方式)。
说人话就是:一个浮点数可以表示如下:
value = sign x exponent x fraction
其中value表示浮点数的实际值
sign(bit)表示符号位: 0表示整数 1表示负数
exponent表示的是转换成科学计数法后的指数偏移值
fraction表示小数部分
知道浮点数的具体表示方式之后,接下来就是要确定每一部分所占的长度。
在IEEE 754标准中,对于32位浮点数的各部分长度约定如下:
·1bit的sign + 8bit的exponent + 23bit的fraction·
而对于64位的浮点数的各部分长度约定如下:
·1bit的sign + 11bit的exponent + 52bit的fraction·
我们前面说过exponent并不是科学计数法之后的实际指数,而是代表科学计数法后的指数偏移量。那么怎么个偏移法呢?
其实在IEEE 754中也对这个做了规定。我们假设k表示exponent所占的总位数,n表示转换成科学计数法之后的实际指数值,那么最终exponent = 2^(k-1) + n
为什么要这么设计呢?我们知道小数可能是不带整数的,这时候如果转换成科学计数法之后实际指数值就应该是负数。
对于指数为负数的情况,我们很自然地会想到用exponent部分的第一位表示正负,然后对于负数值采用补码的方式来表示(取反加一)。
而原来整个value值也有一个sign位表示正负,剩余位在小数为负数的时候也需要使用补码方式来表示。
我们假设这样一种情况:指数为负数且小数为负数,那么对exponent部分的两次取反加1会导致最终结果不可预知。
因此,最后IEEE 754采用了:exponent = 2^(k-1) + n 这种方式来存储指数的偏移值。
我们可以使用如下两行代码来查看0.1分别在32位和64位下的二级制形式:
System.out.println(Integer.toString(Float.floatToIntBits(0.1f), 2)); // 111101110011001100110011001101
System.out.println(Long.toString(Double.doubleToLongBits(0.1), 2)); // 11111110111001100110011001100110011001100110011001100110011010
我们将高位补0,并且按照前面所讲的sign + exponent + fraction的形式将两者拆解如下:
0 01111011 10011001100110011001101
0 01111111011 1001100110011001100110011001100110011001100110011010
要将一个小数转换成浮点数的形式,首先要求得小数的二进制表示法。0.1的整数部分为0,整数部分的如果用8位表示则为:00000000。
小数部分的0.1如何转换成2进制呢?这里我们仍然要从10进制小数来进行推导。
我们假设计算机是以10进制的形式来存储数据的。那么对于0.631,小数部分第1位存储的应该直接就是6,也就是0.631 * 10 的整数部分。
第2位存储的应该就是3,也就是 0.31 * 10 (在第一步去掉整数部分之后再乘以10的整数部分)。同理第3位存储的就是1,0.1 * 10。
于是乎我们可以得到0.1作为二进制在计算机中的存储:
第一位: 0.1 * 2 = 0.2 的整数部分 0
第二位: 0.2 * 2 = 0.4 的整数部分 0
第三位: 0.4 * 2 = 0.8 的整数部分 0
第四位: 0.8 * 2 = 1.6 的整数部分 1 ---》再去掉整数部分后为0.6
第五位: 0.6 * 2 = 1.2 的整数部分 1 ---》再去掉整数部分后为0.2
第六为: 0.2 * 2 = 0.4 的整数部分 0
第七位: 0.4 * 2 = 0.8 的整数部分 0
第八位: 0.8 * 2 = 1.6 的整数部分 1 ---》再去掉整数部分后为0.6
第九位: 0.6 * 2 = 1.2 的整数部分 1 ---》再去掉整数部分后为0.2
.....
综上,我们得到0.1的二进制存储应该为:0001100110011...(0011循环)。
于是,0.1的整个二进制表示为: 00000000.0001100110011...(0011循环)
转换成科学计数法为:1.100110011...(0011循环) * 2^(-4)。
按照IEEE 754标准,如果是32位的表示法,那么exponent = 2 ^ 7 + (-4) = 01111011
如果是64位表示法,则exponent = 2 ^ 10 + (-4) = 01111111011
再按照 sign + exponent + fraction的表示方法拼接起来即得到32位和64位的表示分别如下:
0 01111011 100110011001100110011...(0011循环)
0 01111111011 100110011001100110011001100110011001100110011...(0011循环)
最后剩下的问题就是:小数的存储位数是固定的,那么如果将循环的部分截断呢?这就涉及到舍入规则。
舍入的规则如下:即如果左规或右规时丢弃的是0,则舍去不计,反之要将尾数的末尾加1。
我们同样以0.1为例,32位情况下,小数部分的最终表示如下:10011001100110011001101
我们知道小数部分最后是0011循环,所以最后一位数字本来应该是0,但是因为紧接着的是1,所以最终截取之后还需要进行加1操作,于是就得到1。
64位的表示法同样也可以根据这个规则得到。