浮点数的那点事
浮点数是计算机中储存实数的形式。我们时常需要用浮点数去处理带小数点的运算。可你是否知道,浮点数还有这些操作:
正负无穷大
与整数不同,浮点数没有溢出的概念。当浮点数的运算结果超过一定范围时,它的值就会根据运算结果的符号变成正无穷大或负无穷大。最简单产生无穷大的运算就是除以0.例如1.0/0.0的结果是正无穷大,而-1.0/0.0的结果是负无穷大。注意:浮点数除于0不会产生错误,只有整数才会。
无穷大的性质和你想象中的一样:除了无穷大自身和后面要提到的NaN外,任何一个浮点数都小于正无穷大,大于负无穷大。而且无穷大加、减、除一个非无穷大,或乘一个非0数的结果都是无穷大(可能变号)。
无穷大在一些特定情况很有用。比如说,你要找xx的最小值时,你可以把储存最小值变量初始化为无穷大。或者,你也可以用无穷大的代价表示无解。
NaN
NaN是浮点数中一个有毒的常量。它满足很多奇葩的性质。
NaN是Not a Number的缩写。也就是说,NaN表示它不是一个数字。浮点数中居然有一个“不是数字”的“数字”,究竟是几个意思呢?
首先,我们看看NaN是怎么产生的。NaN对应着数学中的“不定式”,例如0.0/0.0,(1.0/0.0 - 1.0/0.0)(即无穷大减无穷大),0.0*(1.0/0.0)(即0乘以无穷大)等。也就是说,当运算结果不能确定时,它就干脆直接给你一个“不是数字”。
由于NaN不是数字,它满足很多毁三观的性质,其中一个标志性的性质,就是它与任何数字都不相等,包括它自己!
所以,当x取NaN时,x == x为false!
这是判断一个浮点数是不是NaN的常用方法。事实上,除了!=的结果永远是true以外,NaN与任何浮点数的所有大小比较都是false。而且,NaN和任何浮点数的运算(即加减乘除等)结果都是NaN。
由于NaN的性质如此有毒,它很容易造成各种bug。例如,如果你不小心计算了无穷大减无穷大,你就得到NaN。由于它的所有比较都是false,你的程序会出现奇怪的行为,让你百思不得其解。
还有就是,如果你看到如下if语句
if (x < 1.2) { // Do something } else if (x >= 1.2) { // Do something }
你可能以为else后面那个if语句是多余的。其实不然。这个if起到了排除NaN的作用。尤其是当x是函数参数的时候,如果参数传入一个NaN,你要能正确处理它。
当然,NaN也有一定的正面作用。例如我们可以用NaN来表示“无解”或者“不存在”等性质。我们也可以利用它和任意数字不相等这一性质来提供便利。例如如果我们想从浮点数组中删除一个数字,我们可以把它设为NaN。特别的,在C++中我们可以用memset(arr, 0xFF, sizeof(arr))将浮点数组初始化为NaN。
精度误差
由于储存空间有限,我们不可能将一个实数,尤其是无限小数的精确值储存下来。这样,在运算过程中就会产生精度的误差。
我们不妨看这样一段代码:
for (int i = 1; i < 100; ++i) { double a = 1.0/i; if (a*i != 1.0) cout << " " << i; } cout << endl;
假如没有精度误差,上面这段程序应该什么都不输出。但实际上它会输出49和98。这是精度误差导致的相等判断错误。
所以,两个浮点数是否相等并不能直接判断,而应该用两个数的差的绝对值小于一个很小的数(例如1e-7,7代表精确的小数位数)作为条件来进行判断。
const double eps = 1e-7; cout << "inexact result with double:"; for (int i = 1; i < 100; ++i) { double a = 1.0/i; if (!(fabs(a*i - 1.0) < eps)) cout << " " << i; }
这段代码就不会有任何输出了。