L
O
A
D
I
N
G

C/CPP 中关于浮点数计算精度问题及解决办法


在 C/C++ 中,浮点数运算并不像整型那样容易,他往往伴随着一系列的精度问题。而正是这些精度问题,往往会导致程序中一些不可预知错误,而这正是令我们头疼的问题。

一、为什么会精度丢失?

这个问题产生的原因从原理上解释起来并不难。

首先,我们以十进制数 0.9 为例。

然后,我们将十进制数 0.9 转为二进制数(可以用乘二顺取法[1]手算),于是你会得到 0.1110011001100110011... 一个无限循环的二进制小数序列。

然而在计算机中,我们并没有无限的内存供你存放这个无限循环序列,这一点是显而易见的(通常变量允许保存的部分更是少得可怜)。于是,我们就需要对这个序列进行截断。

为了讲解方便,我们就以 12 位来存储这的二进制小数序列

于是,你会得到一个被存储在计算机中的二进制小数:0.11100110011(当然实际存储往往要长一些,在 64 位机中的 double (64bit) 类型变量对小数部分可以存储 52 位[2])。

当你需要再次显示这个数时,你的计算机就需要将这个二进制小数转换回十进制数。

于是,你会得到 0.89990234375 这个数。

看吧,这就是精度丢失!

这一过程你可以用这个网站切身感受:RapidTables(进制转换器)

二、在 C++ 中的浮点数精度问题

首先,让我们从一段程序开始这个问题。

例 2.1
#include <iostream> int main() { float a = 10.0; float b = 0.1; float c = a - b; std::cout << "c = " << c << "\n"; return 0; }
例 2.1 的输出
c = 9.9

这看似没什么问题,实则问题隐藏于冰山之下。

现在,让我们来解开它!

再次尝试下面的程序:

例 2.2
#include <iostream> #include <iomanip> int main() { float a = 10.0; float b = 0.1; float c = a - b; std::cout << std::setiosflags(std::ios::fixed) << std::setprecision(23) << "c = " << c << "\n"; return 0; }
例 2.2 的输出
c = 9.89999961853027343750000

可以看到,当我们输出小数后 23 位后就能发现这个精度缺失问题了。

而例 2.1 之所以能得到正确的答案是因为 std::cout 输出流对小数做了四舍五入操作,通过处理给出了正确答案

看到这,你或许觉得这有什么问题,能得到正确答案不就行了?其实不然,现在让我们将 float a = 10.0; 换为 float a = 500000.0,我们再次运行程序,看看能得到什么(使用默认输出位数)!

a 为 500000.0 时的输出(默认)
c = 500000

好家伙,懵了吧(500000.0 并不是绝对的,准确来说要确保 a 足够的大)!

很明显,这出现了问题!当我们输出小数后 23 位时你就会得到:

a 为 500000.0 时的输出(23位)
c = 499999.90625000000000000000000

所以,即使你可以通过允许误差的方式来判断浮点数是否相等(大概类似于使用 a - b <= 0e-12 来近似代替 a - b == 0.0 这样的判别式),你也无法避免输出显示的问题。而这一切都源于浮点数的精度丢失问题。

三、解决 C++ 中的浮点数精度问题

目前,我尝试的解决办法为++使用 Boost 库++中的 boost::multiprecision::cpp_dec_float_50 来代替 floatdouble 类型。

示例程序
#include <iostream> #include <boost/multiprecision/cpp_dec_float.hpp> #include <boost/multiprecision/cpp_int.hpp> namespace mp = boost::multiprecision; // 命名空间重命名——缩短命名空间 typedef mp::cpp_dec_float_50 float50; // 变量类型名重命名——缩短变量类型名 typedef mp::cpp_dec_float_100 float100;// 变量类型名重命名——缩短变量类型名 int main() { float50 a0("50000.0"); // 推荐声明方式 float50 b0("10.012"); float50 c0 = a0 - b0; float50 a1(50000.0); // 因为 50000.0 不是字符串,所以已经出现了精度丢失。 float50 b1(10.012); // 而用精度丢失后的数再创建高精度变量已经无意义。 float50 c1 = a1 - b1; double a2 = 50000.0; // 演示精度丢失 double b2 = 10.012; double c2 = a2 - b2; std::cout << std::setiosflags(std::ios::fixed) << std::setprecision(25) << "a0\t=\t" << a0 << "\n" << "b0\t=\t" << b0 << "\n" << "c0\t=\t" << c0 << "\n\n" << "a1\t=\t" << a1 << "\n" << "b1\t=\t" << b1 << "\n" << "c1\t=\t" << c1 << "\n\n" << "a2\t=\t" << a2 << "\n" << "b2\t=\t" << b2 << "\n" << "c2\t=\t" << c2 << "\n\n"; return 0; }
输出结果
a0 = 50000.0000000000000000000000000 b0 = 10.0120000000000000000000000 c0 = 49989.9880000000000000000000000 a1 = 50000.0000000000000000000000000 b1 = 10.0120000000000004547473509 c1 = 49989.9879999999999995452526491 a2 = 50000.0000000000000000000000000 b2 = 10.0120000000000004547473509 c2 = 49989.9879999999975552782416344

很明显,精度丢失问题在 Boost 库中得到了很好的解决。当然,Boost 库还有很多其他的关于浮点数操作的优化,而且基本都是基于源标准库的模式重写的,所以使用起来的非常顺手(注意命名空间即可)。


  1. 将小数部分乘 2,取整数部分,最后顺序输出。 ↩︎

  2. 包括 1 个符号位、11 个指数位和 52 个尾数位。 ↩︎


文章作者: SeaYJ
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 SeaYJ !
评论
  目录