浮点数为什么不能判等?
浮点数存储方式
由于数据在计算机中的存储都是以二进制的方式存储,那么十进制中的小数当然也是以二进制的方式存储。
C语言中,对于浮点类型的数据采用单精度类型(float)和双精度类型(double)来存储,float数据占用32bit, double数据占用64bit。无论是单精度还是双精度在存储中都分为三个部分:
- 符号位(Sign) : 0代表正,1代表为负
- 指数位(Exponent): 用于存储科学计数法中的指数数据,并且采用移位存储
- 尾数部分(Mantissa):尾数部分
首先看这段代码:
再看下面这段代码,请问输出结果是什么:
func main() { a, b := 1.5, 1.3 fmt.Println(a-b == 0.2) fmt.Println(a-b > 0.2) }
- 第1行输出,很多同窗应该能知道,浮点数不能直接比较,结果会是false。
- 第2行输出,结果会是true。
若是上面的示例没惊奇到你,那么咱们再看这个示例:
func main() { a := float32(16777216) fmt.Println(a == a+1) a = math.MaxFloat32 fmt.Println(a == a-float32(math.MaxUint32)) }
很神奇,上面这段代码的输出结果是"true true",即咱们的代码认为16777216 = 16777216+1,并且最大的float32数减去最大的32位整形(42亿多)结果竟然仍是等于原值。编程语言
上述“违反常理”问题的缘由与浮点数的计算机表示方式有关。
先来一段通用的,可以较浮点数的方法:
/* f1/f2为待比较的参数,degree为数据的精度 好比:cmpFloat32(1.5, 1.3, 0.000001)返回结果为1 注意:精度degree须要根据实际场景自行调整 */ func cmpFloat32(f1, f2, degree float32) int { if f1 + degree > f2 && f1 - degree < f2 { return 0 // 相等 } else if f1 < f2 { return -1 // f1比f2小 } else { return 1 // f1比f2大 } }
小数的二进制表示
一个小数能够分为3部分:整数部分、小数点、小数部分。
以10.75为例,十进制的转换规则是:10.75 = 1*10^1 + 0*10^0 + 7*10^-1 + 5*10^-2。注意,小数部分取的是模数的负的指数,即模数的指数的倒数。
(对于二进制,转换思路是同样的:10.75 = 1*2^3 + 0*2^2 + 1*2^1 + 0*2^0 + 1*2^-1 + 1*2^-2,因而10.75的二进制就是1010.11 )
对于一个复杂的小数,上述转换公式很难直接写出,因此下面介绍一种方便计算的思路:
- 整数部分,你们很容易想到的编程思路:不断除以2并对2取余获得的0或1便是对应位的二进制值,当整数部分为0时中止。
- 小数部分,则正好与整数相反,不断乘以2,溢出部分会是0或1,这正是小数的二进制值,当小数部分为0时中止。
以10.125为例,整数部分咱们直接给出是1010:
0.125 * 2 = 0.25,整数部分溢出为0,则表示1010.0 0.25 * 2 = 0.5,溢出仍是0,1010.00 0.5 * 2 = 1.0,溢出是1,1010.001 剩余小数部分为0,计算中止,最终结果10.125的二进制表示是1010.001
浮点数精度和精度丢失,为何浮点数是近似表示?
关于浮点数的精度问题,咱们能够经过分析开篇的1.5-1.3 != 0.2案例来解释。
如今咱们将1.5, 1.3, 1.5-1.3, 0.2用前面的打印代码打印出二进制:
浮点数[1.5000]的二进制为[0-01111111-10000000000000000000000] 浮点数[1.3000]的二进制为[0-01111111-01001100110011001100110] 浮点数[0.2000]的二进制为[0-01111100-10011001100110011010000] // 这段是1.5-1.3 浮点数[0.2000]的二进制为[0-01111100-10011001100110011001101] // 这段是0.2
首先,咱们关注下第2行,十进制1.3转换成二进制后是1.01001100110011001100110...,注意后面是循环的,实际上这会是个无限循环小数。一样的,0.2转换成二进制,也是无限循环小数。
当出现无限循环时,须要在没法存储的位上截断掉,此时相似于十进制的四舍五入,二进制下采用0舍1入。咱们观察1.3,紧随后面的截断位应该是0,因此舍去。但0.2的截断处前面1位应该是0,后面1位是1,因而进1,前面的0变成了1。
这就是为何浮点数是近似表示,由于十进制转成二进制后算不尽,有可能出现无限循环小数,此时计算机会将数字截断并做0舍1入取近似值。
相似0.1/0.2/0.3/0.4/0.6/0.7/0.8/0.9这几个数字,都是无限循环的,有兴趣的同窗能够本身用前文的方法计算一遍。
接下来咱们看看浮点数的精度问题。
浮点数[0]的二进制为[0-00000000-00000000000000000000000] 浮点数[0.000000000000000000000000000000000000000000001]的二进制为[0-00000000-00000000000000000000001] 浮点数[16777216]的二进制为[0-10010111-00000000000000000000000] 浮点数[16777217]的二进制为[0-10010111-00000000000000000000000]
上面第2行是float32能表示的最接近0的小数了,再小的话表示不了。此时精度很是高。
但随着数字离0愈来愈远,即除去符号位,数字愈来愈大,精度会慢慢丢失,缘由是指数位能表示的小数点偏移量最大12
为何浮点数不能直接比较?
这个问题跟精度问题是相似的,也是截断引发的。
仍是以1.5-1.3为例:
浮点数[1.5000]的二进制为[0-01111111-10000000000000000000000] 浮点数[1.3000]的二进制为[0-01111111-01001100110011001100110] 浮点数[0.2000]的二进制为[0-01111100-10011001100110011010000] // 这段是1.5-1.3 浮点数[0.2000]的二进制为[0-01111100-10011001100110011001101] // 这段是0.2
咱们将上述浮点的二进制表示转换为二进制小数:
1.5: 1.10000000000000000000000 // 固定在数据域前面添加'1.',下同 1.3: 1.01001100110011001100110 // 无限循环,后面截断了 1.5-1.3:0.00110011001100110011010000 // 注意指数域,小数点左移3位 0.2: 0.00110011001100110011001101
不难算出,第3行+第2行,正好等于第1行(注意遇2则向高位进1位)。
因为1.5和1.3的精度不足,相减后精度没有0.2的精度高,因此上面能够明显看出1.5-1.2和0.2相比,末尾的精度丢失了。
这就是浮点数不能直接比较的缘由。
再看看这部分浮点格式转换
回顾
介绍完上面的理论知识,再回过头来看第一个案例描述中的代码:
double la = 1.345; double lb = 1.123; double l_expected = 2.468; double l_sum = la + lb;
调试这段程序,在内存中查看到la的IEEE754浮点代码是0x3ff5 851e c000 0000,将这个16进制数转换为二进制格式如下:
00111111 11110101 10000101 00011110 11000000 00000000 00000000 00000000
由于阶码在1~2046之间,所以为规格化数,计算尾数时应加上隐合的1。
阶码真值= (0111111 1111) b- (1023) 10=0
尾数真值=1+ (0101 10000101 00011110 11000000 000000000000000000000000) b=1+0.34500002861022 94921875=1.3450000286102294921875
按照同样的计算方法,得出lb的值实际上为1.123 0000257492065,
因此la+lb的值实际上为2.468000 054359436,而事实上 l_expected的值为2.4679999351501465。
这就是案例描述中的软件代码为什么执行flag=2的分支的原因。
解决措施
如何避免浮点数直接判等呢? 可以将浮点变量与浮点数据的相等判断设法转化成区间判断的形式。比如浮点变量x与浮点数据 0.0的相等判断,可以通过如下方式判断: fabs (x) <= err,其中 err为允许误差。按照以上方法,修改案例描述中的软件代码,经调试发现,程序执行了意想之中的flag=1的分支。