在 C/C++ 中,浮点数运算并不像整型那样容易,他往往伴随着一系列的精度问题。而正是这些精度问题,往往会导致程序中一些不可预知错误,而这正是令我们头疼的问题。
一、为什么会精度丢失?
这个问题产生的原因从原理上解释起来并不难。
首先,我们以十进制数 0.9 为例。
然后,我们将十进制数 0.9
转为二进制数(可以用乘二顺取法手算),于是你会得到 0.1110011001100110011...
一个无限循环的二进制小数序列。
乘二顺取法:将小数部分乘 2,取整数部分,最后顺序输出。
然而在计算机中,我们并没有无限的内存供你存放这个无限循环序列,这一点是显而易见的(通常变量允许保存的部分更是少得可怜)。于是,我们就需要对这个序列进行截断。
为了讲解方便,我们就以 12 位来存储这的二进制小数序列。
于是,你会得到一个被存储在计算机中的二进制小数:0.11100110011
(当然实际存储往往要长一些,在 64 位机中的 double (64bit)
类型变量对小数部分可以存储 52 位)。
52 位:包括 1 个符号位、11 个指数位和 52 个尾数位。
当你需要再次显示这个数时,你的计算机就需要将这个二进制小数转换回十进制数。
于是,你会得到 0.89990234375 这个数。
看吧,这就是精度丢失!
这一过程你可以用这个网站切身感受:RapidTables(进制转换器)
二、在 C++ 中的浮点数精度问题
首先,让我们从一段程序开始这个问题。
例 2.11 2 3 4 5 6 7 8 9 10 11
| #include <iostream>
int main() { float a = 10.0; float b = 0.1; float c = a - b;
std::cout << "c = " << c << "\n";
return 0; }
|
这看似没什么问题,实则问题隐藏于冰山之下。
现在,让我们来解开它!
再次尝试下面的程序:
例 2.21 2 3 4 5 6 7 8 9 10 11 12 13
| #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 的输出1
| c = 9.89999961853027343750000
|
可以看到,当我们输出小数后 23 位后就能发现这个精度缺失问题了。
而例 2.1 之所以能得到正确的答案是因为 std::cout 输出流对小数做了四舍五入操作,通过处理给出了正确答案。
看到这,你或许觉得这有什么问题,能得到正确答案不就行了?其实不然,现在让我们将 float a = 10.0;
换为 float a = 500000.0
,我们再次运行程序,看看能得到什么(使用默认输出位数)!
好家伙,懵了吧(500000.0 并不是绝对的,准确来说要确保 a 足够的大)!
很明显,这出现了问题!当我们输出小数后 23 位时你就会得到:
a 为 500000.0 时的输出(23位)1
| c = 499999.90625000000000000000000
|
所以,即使你可以通过允许误差的方式来判断浮点数是否相等(大概类似于使用 a - b <= 1e-12
来近似代替 a - b == 0.0
这样的判别式),你也无法避免输出显示的问题。而这一切都源于浮点数的精度丢失问题。
三、解决 C++ 中的浮点数精度问题
目前,我尝试的解决办法为使用 Boost 库中的 boost::multiprecision::cpp_dec_float_50
来代替 float
或 double
类型。
示例程序1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #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); 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; }
|
输出结果1 2 3 4 5 6 7 8 9 10 11
| 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 库还有很多其他的关于浮点数操作的优化,而且基本都是基于源标准库的模式重写的,所以使用起来的非常顺手(注意命名空间即可)。