珍爱生命,远离浮点数
这句话绝不是空穴来风,而是一个经验之谈。这里我们将讨论浮点数的精度问题以及一些其他的浮点数问题。
浮点数
浮点数是一种数据类型,用来表示小数。
根据 IEEE 754 标准,浮点数可分为符号位(sign bit)、指数位(exponent bits)、尾数位(fraction bits)三部分。
浮点数的原理类似于科学计数法,即按照指数和尾数的形式表示浮点数。所以可以表示很大或很小的数字。
然而,浮点数真的足够精确吗?
精度问题
很遗憾,浮点数还不够精确。
由于浮点数的存储方式,只有有限的数可以被精确表示,而我们常见的 0.1
和 0.2
等并不能以有限的二进制方式表达,而是在小数部分的末尾多了个“尾巴”。
由于这个特性,很多数学运算都会出现精度问题。
比如最经典的一个例子:
#include <stdio.h>
int main()
{
printf("%.17f\n", .1 + .2);
return 0;
}
最终得到的结果:
0.30000000000000004
更多内容可以参考 这个网站。
其他问题
-
+0.0
和-0.0
在计算机中,由于符号位的存在,所有的浮点数都是有符号的,包括
+0.0
和-0.0
,而这两个浮点数的表示不同。 -
inf
和NaN
计算机中有两种特殊的浮点数:
inf
和NaN
,inf
表示正无穷,-inf
表示负无穷,NaN
表示不是数字的值。当遇到非常大的浮点数时,其会被转换为
inf
,比如1e400
。当遇到非数字的值时,其会被转换为NaN
,比如inf - inf
。IEEE 754 标准“贴心”地提供了它们的计算法则:
inf + inf
=inf
inf - inf
=NaN
inf * inf
=inf
inf / inf
=NaN
inf * 0
=NaN
NaN == NaN
=false
NaN < NaN
=false
NaN > NaN
=false
- ...
其中,
NaN
经过任何运算都会变成NaN
,但NaN
不等于NaN
。这样的计算特性也带来了一些问题,比如:
- 在复杂的浮点数计算中,
inf
和NaN
会在计算过程中不断传递,导致得不到预期的结果。
解决方案
-
化“浮”为“整”
最经典的例子是处理货币问题,我们可以把金额转换为更小的单位。比如:
#include
int main() { printf("请输入金额(分):"); int money = 0; scanf("%d", &money); printf("请输入实付金额(分):"); int real_money = 0; scanf("%d", &real_money); int return_money = real_money - money; printf("应退金额:%d分\n", return_money); printf("即%.2f元\n", (double)return_money / 100.0); return 0; } -
使用 decimal 库
在 C++ 中,我们可以使用
decimal
库来解决这个问题。#include
#include int main() { std::cout << "请输入金额(元):"; decimal::Decimal money; std::cin >> money; std::cout << "请输入实付金额(元):"; decimal::Decimal real_money; std::cin >> real_money; decimal::Decimal return_money = real_money - money; std::cout << "应退金额:" << return_money << "元\n"; return 0; } 在其它语言中也可以使用
decimal
库,这里请读者自行查阅相关文档。