问题

为什么在项目中要尽量避免使用浮点数,不使用浮点数,那该如何计算浮点数?

回答
在软件开发的世界里,浮点数就像一个充满诱惑却又潜藏暗礁的宝藏。它们能够表达连续的数值,看起来无所不能,但一旦不加小心地使用,带来的麻烦可能远超你的想象。这篇文章,我们就来好好聊聊为什么要在项目中尽量规避浮点数,以及如果真的需要处理这些“小数”,我们有哪些替代方案。

为何浮点数是个“坑”?避之不及的理由

简单来说,浮点数之所以容易出错,是因为计算机处理它们的方式。我们脑子里想的“0.1 + 0.2 = 0.3”在计算机底层并非如此精确。这背后涉及几个关键原因:

1. 二进制的宿命:并非所有十进制小数都能完美表示

我们日常使用的十进制,可以轻而易举地表示“0.1”、“0.2”这些数字。但计算机的世界里,一切皆为二进制。而很多我们熟悉的十进制小数,在转换成二进制时,会出现无限循环小数的情况。

想象一下,十进制的 1/3 = 0.3333...,你想在有限的格子里把它装进去,势必会截断,损失精度。浮点数在二进制里也是类似。比如,十进制的 0.1,用二进制表示就是 0.00011001100110011...,这是一个无限循环的序列。

为了存储,计算机必须在某个地方“截断”这个无限循环。这个截断就会引入微小的误差。而且,这种误差是累积的。当你进行一系列浮点数运算时,这些微小的误差就会像滚雪球一样,越滚越大,最终可能导致你意想不到的结果。

举个例子: 你在做一个电子表格,计算一个项目的成本。你输入了 0.1 元的费用,然后复制了 10 次,理论上应该是 1.0 元。但由于前面提到的二进制表示误差,计算结果可能变成了 0.9999999999999999 或者 1.0000000000000001。在很多情况下,这或许可以接受,但如果你的程序对精度要求非常高(比如金融计算、科学模拟),这种误差就可能是致命的。

2. 比较的陷阱:永远不要直接用“==”比较浮点数

这是使用浮点数最常见也最容易犯的错误之一。由于前面提到的精度问题,两个本应相等的浮点数,在计算机内部的表示可能非常非常接近,但并不完全一样。

场景: 你在游戏中,判断一个玩家的生命值是否为零。如果你用 `player_health == 0.0` 来判断,那么当 `player_health` 是一个非常小的负数(比如 0.00000000000001)时,这个判断就会失败,即使从逻辑上来说玩家已经死了。

正确的做法是比较它们之间的差是否在一个很小的范围内,这个范围通常被称为“容差”(tolerance)或“epsilon”。例如,`abs(player_health 0.0) < epsilon`。但这又引入了另一个问题:如何确定这个 epsilon 呢? 这个值需要根据你的具体业务逻辑来定义,而且选择不当同样会带来问题。

3. 性能考量:浮点运算通常比整数运算慢

虽然现代处理器对浮点运算有专门的优化,但在某些情况下,整数运算仍然比浮点运算更快。尤其是在一些嵌入式系统或者对性能有极致要求的场景下,能避免浮点数就尽量避免。

4. 平台差异:不同硬件或编译器可能对浮点数处理有细微差别

虽然 IEEE 754 标准统一了浮点数的表示,但不同硬件、不同编译器、甚至不同的优化级别,都可能对浮点运算的结果产生细微的影响。这可能导致你的程序在一个环境下运行正常,在另一个环境下却出现奇怪的 bug。

好,不让浮点数“占便宜”,我们该怎么办?—— 精巧的替代方案

既然浮点数是如此“麻烦”,那我们该如何处理需要表示小数的场景呢?别担心,我们有几种更健壮、更可控的方法:

1. 定点数 (FixedPoint Arithmetic)

定点数是一种古老但非常有效的处理小数的方法。它的核心思想是:我预先约定好小数点的位置,并把所有数字都当作整数来存储。

怎么做?

约定尺度: 例如,我们决定所有数字都乘以 100 来存储,这样小数点后两位就是固定的。
存储整数: 当你需要存储 12.34 时,你实际存储的是 1234。当你需要存储 0.50 时,你存储 50。
运算时处理:
加减法: 直接对存储的整数进行加减,结果是正确的。
乘法: 如果你将两个“放大”过的数相乘,比如 12.34 0.50,你实际计算的是 1234 50 = 61700。因为原始数都放大了 100 倍,所以乘积需要再除以 100² (10000) 才能得到正确的结果:61700 / 10000 = 6.17。
除法: 类似地,如果你计算 12.34 / 2.00,你计算的是 1234 / 200 = 6.17。在除之前,要先将分子“放大”到与分母相同的尺度,然后除法的结果也需要根据总的放大倍数进行调整。通常,如果你将数字放大 M 倍,那么除法 `a / b` 就变成 `(a M) / b` (如果a是整数)或者 `(a M) / (b M)` (如果a和b都要放大)。 更严谨地说,如果你的数字是 `value 10^n` 的形式,那么存储的就是 `value`,运算时需要考虑 `10^n` 的因子。

优点:

精度可控: 你可以根据需要选择小数点后的位数,保证了运算的精确性。
避免比较问题: 因为存储的是整数,比较是完全准确的。
性能好: 通常性能接近整数运算。
平台独立性: 结果在不同平台上是完全一致的。

缺点:

手动管理: 需要手动处理放大和缩小的过程,代码会稍微复杂一些。
数值范围受限: 如果需要表示非常大的数或非常小的数,可能会遇到整数溢出的问题,或者需要更大的存储空间。
不支持科学计数法: 比如 3.14e5 这种表示形式,定点数是无法直接表示的。

适用场景: 金融计算(例如货币处理)、需要高精度的计数器、嵌入式系统中的数字信号处理等。许多老式计算器和一些金融软件内部都采用定点数。

2. 使用专门的高精度库或货币库

如果你不想自己实现定点数的逻辑,可以直接使用现成的库。很多编程语言都有提供高精度运算的库,例如 Python 的 `Decimal` 模块,Java 的 `BigDecimal` 类,或者一些专门为金融计算设计的库。

这些库通常内部就是使用了类似定点数的技术,但它们帮你封装好了所有的细节,提供了更方便的 API 来进行加减乘除、格式化输出等操作。

优点:

方便易用: 提供直观的 API,大大简化了开发工作。
功能强大: 通常支持更多的数学函数和格式化选项。
精度高: 可以配置非常高的精度,远超标准浮点数。

缺点:

性能开销: 相较于原生整数运算,高精度库通常会带来一定的性能损耗,因为它们需要额外的逻辑来处理任意精度。
引入依赖: 需要引入额外的库到你的项目中。

适用场景: 金融应用、需要精确表示不确定的小数(如科学计算中的某些中间结果)等。

3. 将“小数”理解为“比例”或“计数”

在某些情况下,你可能不需要直接计算小数本身,而是需要表示一种比例关系,或者通过计数来间接表示小数。

举例:

百分比: 如果你需要表示 50% 的折扣,你可以不存储 0.50,而是存储一个“折扣系数”为 50,然后约定这个系数表示百分之多少。或者直接存储一个比例因子,比如 1/2。
分数: 对于某些固定的比例关系,比如 1/3、2/3,可以直接将它们表示为分数对象(一个分子和一个分母)。运算时,你需要实现分数运算的规则(通分、约分等)。

优点:

概念清晰: 如果业务场景适合,这种方法可以使代码逻辑更易于理解。
避免浮点数误差: 完全规避了浮点数带来的问题。

缺点:

通用性差: 这种方法高度依赖于具体的业务场景,不适用于所有需要小数的场合。
实现复杂度: 实现分数运算、比例转换等逻辑可能比较繁琐。

适用场景: 某些特定比例的计算、简单的百分比表示等。

如何选择?

选择哪种方法取决于你的具体需求:

如果对精度要求极高,且业务场景固定(如货币): 定点数或高精度库是最佳选择。
如果对性能有极致追求,且可以接受一定程度的手动管理: 自己实现定点数。
如果想快速实现高精度运算,且性能不是最关键的瓶颈: 使用现成的高精度库。
如果可以业务逻辑上绕开小数,用整数计数或比例表示: 这是最理想的,但并非总是可行。

总结一下

浮点数就像一把双刃剑,用好了能解决很多问题,但稍有不慎就可能带来难以察觉的 bug。在项目中,我们应该尽量从设计阶段就考虑如何规避它们带来的风险。通过采用定点数、高精度库或者调整业务逻辑,我们可以构建出更稳定、更可靠的软件。记住,精确的计算是许多应用的基石,而避免浮点数,正是迈向更高精确度的重要一步。下一次你在项目中遇到需要处理小数的时候,不妨想想这些替代方案,让你的代码更加健壮!

网友意见

user avatar

「在项目中要尽量避免使用浮点数」本来就是个伪命题,很大程度上是以讹传讹得来的,比方说上面回答中那个猴子的故事就很形象。

这个以讹传讹的本体来源于工程需求,往往是要让人尽量用整数,却忽略了整数跟浮点同样都是有限精度。

如果你需要无限精度,那么只有自定义类型能满足。整数vs浮点并没有太大的优势。

实际上,浮点的误差来自十进制小数跟二进制小数之间的转换。如果这个转换对你具体的应用来说不是必须的,那么你就完全可以用浮点。

因而,不应该使用浮点数的常用场合只有一个,那就是:当你需要让一个变量精确的对应到现实中某个十进制小数时,不应当使用浮点小数来保存这个变量。当十进制小数与二进制浮点小数的转换不可避免,你又必须要求精度时,不应当使用浮点。

比方说 1.1 元钱,因为这个变量必须精确的等于十进制的 1.1,而变量本身是二进制,此时进制转换是必须的,所以你用浮点小数保存是错误的,你可以考虑用精确到分的 110 来表示这个数字,避免了小数。类似的,如果是计算器的话,如果用户是用十进制的格式输入,那么直接转换成二进制用计算机进行运算,最后再转换成十进制显示,这就不可避免的会有误差。

如果没有进制转换的需求,那么用浮点没有任何问题,甚至不会有精确性方面的问题

那么举一些可以用浮点数例子:

1,如果你有一个只需要在计算过程中出现的变量,它的输入并不来源于十进制小数,也不需要被转换成十进制保存,那么它完全可以用浮点,最典型的就是游戏,你需要使用一个很复杂的增伤减伤公式来计算出实际的伤害,而这个伤害直接的被作用到了目标,在整个过程中,所有的操作都使用二进制进行,不存在二进制十进制转换,因此就不会产生误差,此时用浮点根本没有问题。——当然在游戏中,即便其中的变量最终需要以十进制显示,你仍然可以用浮点,因为十进制显示虽然有误差,但可以四舍五入显示,不需要绝对精确。

2,如果你的浮点仅仅用来存储精度内的整数,那么使用它也不会存在问题。例如 64 位浮点可以用来精确存储 2^47 以内的整数,不会存在任何误差。换句话说就是 64 位浮点可以无损的放进一个 32 位的 int,不存在任何误差,在 32 位整数不够用,系统又不支持 64 位整数时,它也可以被当作 47 位的整数类型使用。(47这个数字有争议,有说48,52,53等等的,但不影响具体结论,都大于32,装32位整数依然可以完全精确表示。)

同理,32 位的浮点则可以精确的放进一个 2^23 以内的整数,不存在任何误差。 32 位浮点变量保存一个 16 位的 short 类型整数不产生任何误差,它也可以被当作一个精度在 23 bit 的整数类型。

3,可能存在的其他例子,欢迎补充。


--


补充,其实有一个不应该使用浮点的不常用场合,就是目标cpu没有浮点处理器,此时的浮点使用整数模拟计算出,效率极低。鉴于现代的绝大多数大多数CPU都配备了浮点处理器,浮点运算的性能跟整数是相当的。如果使用现代的cpu,需要避免浮点的情况发生概率很小。但在一二十年前,有很多cpu(尤其是嵌入式cpu)还没有配备浮点处理器,此时确实应该尽量避免,甚至完全避免用浮点。

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有