有同事在工作中遇到了一个奇怪的问题,这个问题与浮点数计算相关,代码如下:
1 #include
2 using namespace std;
3
4 int main()
5 {
6 double s = 6.0;
7 double e = 0.2;
8
9 cout << static_cast(s/e) << endl;
10 return 0;
11 }
这段代码看起来很简单,心算一下,应该输出30才对。
但结果却是我们在32 和 64位 linux平台下得到了不同的结果,分别是29和30,意想不到对吧?
然后,如果把代码改成如下:
1 #include
2 using namespace std;
3
4 int main()
5 {
6 double s = 6.0;
7 double e = 0.2;
8
9 double d = s/e;
10
11 cout << static_cast(d) << endl;
12 return 0;
13 }
你会发现在两个平台上都得到了相同的“正确”结果!为什么呢?
稀疏的浮点数
众所周知,计算机是无法精确地表示所有浮点数的,无理数的稠密性使得无论我们用多高精度的数据类型来表示浮点数,所能表示的范围相对整个无理数来说都是相当相当地稀疏疏。
因此在计算机的世界里,我们只能尽可能用有限精度地来表示一定范围的数据,至于那些没法精确表示的数字,就只能在计算机所能表示的范围里找一个和它最接近的数来凑和凑和。
这个好像比较好理解,比如说根号2什么的,我们都知道这些无理数不能在计算机里完全精确的表示,但还有那么一些有理数,在10进制里虽然可以精确地表示,在2进制里却也是无法精确表示的,比如说上面例子中的0.2,你如果对此有怀疑,可以好好回顾一下怎么把小数转成二进制,然后慢慢用笔在纸上演算一下。
讲这些,无非还是想说明,计算机世界里的浮点数是相当疏松地,借用《深入理解操作系统》一书里的一张图,让大家
浮点数的折断与转换
因为很多小数是无法精确表示的,因此我们只能尽可能在有限精度的小数里找到最近接近的数来近似那些无限的小数。
那么计算机是怎么样来做这些逼近的呢?常用的有如下4种方式:
其中第一种是默认使用方式,需要注意的是这些折断方式并不仅限于由浮点数转为整数,浮点数之间也是适用的。
在C语言中,浮点数与整数的转换有以下几条原则:
1) int型转为float,不会overfloat,但有些数用float无法表示,因此可能需要rounding,记住float很稀疏。
2) 由int或float转为double时,精度不会丢失,毕竟double精度高太多了。
3) double转为float时,很可能会overfloat, 转换则用round-to-even的方式(默认)进行。
4) 由float, double转换为int时用round-to-zero的方式转换,当然也很可能会被截断。
请注意第3条,第4条原则,它们转换时使用的不同原则有时会导致一些很微妙的结果。
Intel IA32 浮点运算
IA32处理器和很多其它一些处理器一样,有专门用于保存浮点数的寄存器,当在cpu中进行浮点数运算时,这些寄存器就用来保存输入输出及相关的中间结果。
但IA32有一个比较特别的地方,它的浮点数寄存器是80位的,而我们在程序中只用到32和64位两种类型,因此当把float,double放入到cpu中时,它们都会被转换成了80位,然后以80位的方式进行运算,最后得到的结果再转换回来。这样特性使得浮点数的计算可以相对更精确些,但同时,一不小心很可能也会引出一些意想不到的问题。
你可能突然恍然大悟了,对的,我们最开始提到那个奇怪的问题就与此相关。
s/e得到结果是个80位的浮点数,由这个浮点数先转换成double再转成int,与直接就转换成int,结果很可能是不同的。
比如在我们的例子中,s/e ~ 29.999999....时,s/e转换成double使用round-to-even的方式,会得到也许是30.0000001,再转成整形时,得到30.
但如果直接由29.99999...转换成整型,得到却是29。