# 浮点数

数学中稠密的实数无法利用有限的计算机储存进行表示,出于精度和范围的平衡,目前世界上最广为使用的浮点数标准是 IEEE 754 标准

通常来说,浮点数的精度分为

  • 单精度(Float):占用 4 字节,32 位,约 7 位十进制有效数字,在 C 语言中通过后缀 f/F 来表示,例如 0.5F
  • 双精度(Double):占用 8 字节,64 位,约 16 位十进制有效数字,在 C 语言中默认的浮点数类型,例如 0.5

在该标准中,一个 32 位的浮点数被划分为三个区域:

  • 符号位(Sign, 1 bit):0 表示正数,1 表示负数
  • 指数位(Exponent, 8 bits):采用偏移码(Biased representation)存储,决定了数值的量级范围。
  • 尾数位(Fraction/Mantissa, 23 bits):存储有效数字。在规格化表示中,小数点前默认有一个隐藏的 “1”,因此实际有效位数为 24 位。

其数学表达式通常可抽象为:

V=(1)S×(1+Fraction)×2(EBias)V = (-1)^S \times (1 + \text{Fraction}) \times 2^{(E - \text{Bias})}

关于浮点数,需要知道以下事实:

  • 计算机中小数等价为 22 的负幂次方的和,因此 0.5F 严格等于 0.50.5,同时 0.1F 略微大于 0.10.1,而 0.7F 略微小于 0.70.7
  • 等同于整数的浮点数会被修正

参考以下 C 语言代码

#include <stdio.h>
int main() {
    float a = 0.1F;
    float b = 0.7F;
    float c = 16777217.0F;
    printf("a=%.10f\n", a);
    printf("b=%.10f\n", b);
    printf("c=%.10f\n", c);
}
// output:
// a=0.1000000015
// b=0.6999999881
// c=16777216.0000000000

该类错误被称为舍入误差「丸め誤差」,是由于浮点数的有限精度导致的。
舍入误差会导致运算的结合律不成立,例如 (0.1F + 0.3F) + 0.5F0.1F + (0.3F + 0.5F) 的结果分别为 0.89999997620.9000000358

虽然我们知道 0.1F 略大于 0.10.1,但是如果将其累加十次,会变得比 11 更大,这种现象叫做信息丢失「情報落ち」,是在运算过程中,由于有效位数不足,小量被大数 “吞掉”,或者做差时前面很多位抵消,导致有用信息丢失。

#include <stdio.h>
int main() {
    int i;
    float x,s;
    x=0.1;
    s=0.0;
    for(i=0;i<10;i++) s=s+x;
    printf("s=%.10f\n", s);
}
// output:
// s=1.0000001192

关于浮点数的误差,还需要知道的是有效数字丢失「桁落ち」,这是两个非常接近的数相减时,前面许多相同的位被抵消掉,导致结果的有效数字大幅减少的现象。
让我们看以下二次方程求根代码示例

#include <stdio.h>
#include <math.h>
int main() {
    float a,b,c,d,x1,x2;
    a=2; b=12340.0; c=3;
    d=b*b-4*a*c;
    printf("a=%f b=%f c=%f d=%f\n", a, b, c, d);
    x1=(-b+sqrt(d))/(2*a);
    x2=(-b-sqrt(d))/(2*a);
    printf("x1=%8f\n", x1);
    printf("x2=%8f\n", x2);
}
// output:
// x1=-0.000162
// x2=-6170.000000

然而,我们不妨代入计算可以得到

x1=12340+123402423220.000243,x2=12340123402423226170.000000x_1 = \frac{-12340 + \sqrt{12340^2 - 4 \cdot 2 \cdot 3}}{2 \cdot 2} \approx -0.000243,\quad x_2 = \frac{-12340 - \sqrt{12340^2 - 4 \cdot 2 \cdot 3}}{2 \cdot 2} \approx -6170.000000

可以看出,虽然 x2x_2 的结果是正确的,但 x1x_1 的结果出现了大幅错误,这是因为分子两项的值过于接近
这种情况下应该使用 x1=cax2x_1 = \dfrac{-c}{a x_2} 来计算 x1x_1,以避免有效数字丢失