关于计算机存储浮点数、一次有趣的问题探索(0.1+0.2=0.3?)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Three_dog/article/details/83005097

干货请直接翻到第三个大标题开始看,其他都是随笔杂谈的部分

晚上八点,同事在群里扔出一段有趣的代码

#include <iostream>
using namespace std;
int main(){
	double num = 55.3;
	int i = num;
	double ta = (num - i) * 100;
	int tb = ta;
	cout << ta << "  " << tb << endl;
	return 0;
}

包括本人在内,一大波儿程序员在下班时间开始了口算心算代码算的装逼之路。意料之中的是,编译器给出的答案果然和我们这等凡夫俗子不一样:30 29,什么情况?一个数转成了int直接数值少了1? 接下来就是排坑查错的过程,这过程对程序员来说也往往是最有趣也最难熬的过程。

众人拾柴

人多思路多,环境多办法也多, 在测试了不同操作系统、不同编译器,不同位数(x32还是x64)等等之后,得到的答案出奇的一致。那么出现这个问题的原因,肯定在代码里。事已至此,只能搬出程序员的调试神器print,cout,alert了。(有考虑过可能是cout输出的原因,用printf测试过结果依旧,在这里不多费笔墨了)

#include <iostream>
#include <stdio.h>
using namespace std;
int main(){
	double num = 55.3;
	int i = num;
	double ta = (num - i) * 100;
	cout << ta << endl; // 打印一下ta的值
	int tb = ta;
	cout << ta << "  " << tb << endl;
	return 0;
}

结果仍然很意思, 打印显示ta初始化之后的值就30,而tb初始化后的值是29,几个意思?一个double类型的30,赋值给一个int类型的变量成了29呗?于是我怀揣着一丝侥幸,声明了一个变量int a = 30.000 期待它真的能等于29,编译器很给面子,毫不犹豫的给了我一个30的答案并骂我脑子有病。既然是这样,那答案就只有一个了,虽然听上去有点玄乎:你看到的30,并不是30

黔驴技穷

这就有点神奇了,为了探究问题究竟是属于计算机科学还是玄学,同事们拿出了大量的数据进行测试,得出的结果差点要把我们引向玄学的深渊:代码中的55.3,换成55.(123456789),小数位为:4、5、8、9 会有问题,而其他的情况下正常两个数字保持一致。小数位换完再换整数位,从11.3到22.3到99.3,结果是除了11.3和22.3正常以外, 其他无一幸免,赋值后的两个值并不一样。这就有点玄乎了? 好使不好使还得看你编译器的脾气?

探讨到这里,群里已经有同事意识到问题出在了哪里了,其实就是计算机存储浮点数的问题,各位同事们也在恍然大悟之后进行了亲切友好的弹冠相庆,深入贯彻落实了不给钱不加班,不用身体换金钱的科学编程观,去吃鸡了。

知识点有没有?

剩下吃鸡技术菜到抠脚的我来说说这个你看到的30,并不是30是怎么回事:
这其实是计算机编程里一个老老生常常谈的问题:浮点数的存储。网上有很多相关的帖子,深究的话大家可以去直接百度0.1+0.2=0.3,你会看到铺天盖地的技术详解。我这里尽可能简单的说一下是怎么回事:计算机存储都是用二进制存的,那么小数诸如0.1、0.2、0.3存储在内存中,使用二进制无法完全等值,只能无限接近,类似这样:

十进制 二进制
0.1 0.0001 1001 1001 1001 …
0.2 0.0011 0011 0011 0011 …
0.3 0.0100 1100 1100 1100 …
0.4 0.0110 0110 0110 0110 …

这样呢,就导致一个问题:0.1+0.2 是两个二进制数相加,因为本身这两个数字在计算机底层存储的时候并不精确, 所以他们相加的结果和0.3对应的二进制值并不相等,只是非常接近。这就是为什么在有些编程语言中0.1+0.2!=0.3

在有些编程语言比如我们这个程序中使用的C++中,编译器给我们做了一定程度的优化,当这个数字无限接近0.3的时候,它认为你想要的就是个0.3,那么它给你打印出来就是个0.3, 但是显示归显示,它在内存中存的值并没有变,还是一个0.299999…在底层仍然是一个很接近0.3的二进制数值的一个二进制数值。然后编译器再拿他进行赋值进行运算的时候,就出现了上面遇到的问题。

我们用55.3-55得出的是一个0.299999…的数,乘100后变成了29.999999…,打印的时候,编译器为了照顾你的情绪,给了你看的是30,但正儿八经拿他赋值给一个int,它便原型毕露。 这就好比猴孩子作业没写完他告诉你他写完了,你信了就完事儿了结果到学校经不住老师检查,转眼班主任办公室里又出现一个精神矍铄,毛发稀松的中年程序员的身影…

口说无凭

我们还遗留两个问题:

  1. 以上都只是我说,编程里并没有得到验证
  2. 为什么有些值可以,有些值不行?还不是一错都错?

1、编程验证

我既然说了C++编译器是个聪明孩子,它会考虑你的感受适当的善意的欺骗你,那他就给不了我们想要的真相,我们换成python来做上面的事情,结果就很清晰了:

num = 55.3
i = int(num)
ta = float((num - i) * 100 )
print(ta)
tb = int(ta)
print(ta,'  ',tb)

结果如下:
在这里插入图片描述
可见在这件事情上,python保留了一个"年轻人"应有的坦诚与直率,你55.3-50得到的就是一个29.999999999716,虽然这不符合数学逻辑,但是浮点数存储在二进制里,它只能做到这一步。这也验证了上面说的,在这段C++程序中,你看到的30,并不是30 。

2、谁行谁不行?

这样的话,为什么55.3、55.4会出现这个问题,但是55.1、55.2等等就不会出现两个值不一样的情况呢?看Python中的以下输出就很清晰了::
在这里插入图片描述
显然这个计算的出的值,由于浮点数存储的原因,它不完全等于,可能大可能小。像0.100000000142在乘100后再转成int,并不会对它0.1这个数值造成影响,所以55.1 55.2并不会出现此BUG。
不仅如此,同样是0.3,通过不同方式计算得来的结果也不相同, 也从另一个侧面印证了我们上面的理论:
在这里插入图片描述

一拳一个意义怪

问题探索完了,一个必然的哲学问题摆在脸前:费劲巴拉纠结这有啥意义? 编程的时候留意一下避免就好了。
答案确实是没啥意义,对我而言只是满足了一个技术控强烈的好奇心而已,非要强行纠缠出些意义的话,我觉得有两点:

  1. 面试的时候,人家问出来,你会,你知道,你讲的明白,可以狠狠装一波儿逼。
  2. 面试别人的时候,你问出来,他不知道,不会,说不清楚,你可以狠狠装一波儿逼。

补充一条干货:如果面试中出现这种问题。答案是:可能是30 29, 可能是30 30。

猜你喜欢

转载自blog.csdn.net/Three_dog/article/details/83005097