Skip to content

4.8 - 浮点数

Key Takeaway
  • 浮点数数据类型有三种:float(4 bytes), double(8 byte) 和 long double(8、12、16byte)
  • 浮点数数据类型始终是有符号的
  • 在使用浮点数字面量时,请始终保留一位小数
  • 默认情况下浮点数字面量为double。使用f后缀可以标注该字面量为float
  • 请确保你使用的字面量和它赋值的类型是匹配的。否则会发生不必要的转换,导致精度丢失
  • 如果内存空间允许,尽量使用 double 而不是 float,因为 float 的精度更低,也就有可能造成数值不准确的问题
  • 舍入误差始终存在,它是规则的一部分,永远不能假定浮点数是精确的

整型可以很好地表示整数,但是很多时候我们需要保存非常大的数,或者小数。浮点数类型的变量可以用来存放实数,例如4320.0、-3.33 或 0.01226。浮点数名字中的浮点二字,形象地说明了小数点可以浮动;也就是说它可以支持小数点前和小数点后具有不同位的数字。

浮点数数据类型有三种:floatdouble 和 long double。和整型一样 C++ 并没有定义这三种类型的具体长度(只确保其最小值)。在现代计算机上,浮点数的表示方法几乎总是遵循 IEEE 754 二进制格式。在这种格式下,float 为4个字节,double则是 8 个字节,而long double可以和double相同(8字节),也可能为80位(通常会补位到12字节)或者16字节。

浮点数数据类型始终是有符号的(可以保存正负数)。

Category Type Minimum Size Typical Size
floating point float 4 bytes 4 bytes
double 8 bytes 8 bytes
long double 8 bytes 8, 12, or 16 bytes

下面是一些浮点数的例子:

1
2
3
float fValue;
double dValue;
long double ldValue;

在使用浮点数字面量时,请始终保留一位小数(即使这一位是0)。这可以帮助编译器明确该数值为浮点数而非整型数。

1
2
3
int x{5}; // 5 表示整型数
double y{5.0}; // 5.0 是浮点数字面量 (没有后缀的情况下默认为 double)
float z{5.0f}; // 5.0 是浮点数字面量,f 后缀表示 float

注意,默认情况下浮点数字面量为double。使用f后缀可以标注该字面量为float

最佳实践

请确保你使用的字面量和它赋值的类型是匹配的。否则会发生不必要的转换,导致精度丢失。

注意

请确保在应当使用浮点数字面量时,不要误用整型字面量。这可能会发生对浮点类型对象初始化、赋值、数学运算以及调用返回值应当为浮点数的函数时。

打印浮点数

考虑下面这个简单的程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>

int main()
{
    std::cout << 5.0 << '\n';
    std::cout << 6.7f << '\n';
    std::cout << 9876543.21 << '\n';

    return 0;
}

上述程序的输出结果可能会让你感到意外:

1
2
3
5
6.7
9.87654e+06

在第一条打印中,std::cout 打印 5,即使我们输入的是5.0。默认情况下 std::cout 不会输入小数部分的0。

在第二条打印中,结果和我们期望的是一样。

在第三条打印中,打印结果为科学计数法(如果你需要复习一下科学计数法可以参考 4.7 - 科学计数法)。

浮点数的范围

假定使用 IEEE 754 表示法:

大小 范围 精度
4 bytes ±1.18 x 10-38 到 ±3.4 x 1038 6-9 位有效数字,一般为7
8 bytes ±2.23 x 10-308 到 ±1.80 x 10308 15-18 位有效数字,一般为16
80-bits (typically uses 12 或 16 bytes) ±3.36 x 10-4932 到 ±1.18 x 104932 18-21 位有效数字
16 bytes ±3.36 x 10-4932 到 ±1.18 x 104932 33-36 位有效数字

80位的浮点数类型可以看做是历史遗留问题。在现代处理器上,它通常被实现为12字节或16字节(对于处理器来说是更加方便的长度)。

你可能会奇怪为什么80位的浮点数类型和16字节的浮点数类型具有相同的范围。这是因为它们中专门用来表示指数的位是相同的——不过,16字节的浮点数可以表示更多的有效数字。

浮点数的精度

考虑以下分数 1/3。它的十进制表示法为 0.33333333333333… 无限循环3。如果你在纸上一直写的话,写着写着你就受不了了。最终你写下来的可能是 0.3333333333…. (无限循环3),但绝对不可能是全部的值。

对于计算机来说,无限长度的数字需要无限大的内存才能存储,而通常我们只能使用4字节或8字节的空间。有限的内存就意味着浮点数只能存放特定长度的有效数字——其他剩余部分就被丢弃了。最终被存储下来的数字是我们能够接受的值,而不是实际值。

浮点数的精度被定义为:在不损失信息的情况下能够表示的最多的有效数字位数。

在输出浮点数时,std::cout的默认精度为6——也就说它假设所有浮点数都只有6位有效数字,超过的部分都会被截断。

下面的程序对 std::cout截断到6位有效数字进行了演示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>

int main()
{
    std::cout << 9.87654321f << '\n';
    std::cout << 987.654321f << '\n';
    std::cout << 987654.321f << '\n';
    std::cout << 9876543.21f << '\n';
    std::cout << 0.0000987654321f << '\n';

    return 0;
}

输出结果为:

1
2
3
4
5
9.87654
987.654
987654
9.87654e+006
9.87654e-005

我们注意到,输出结果中的数都只有6位有效数字。

同时我们注意到, std::cout 有些情况下会自动改用科学计数法。根据编译器的不同,指数部分会被补0到最小位数。不要担心,9.87654e+006 和 9.87654e6 是完全一样的,只是补了一些0罢了。指数的最小位数根编译器有关,Visual Studio 是 3,其他编译器可能会使用C99的标准即2。

浮点数精度取决于浮点数类型的大小(float的精度小于double)以及被存储的具体值(有些值的精度本身就比其他值高)。float 类型的精度通常位于 6 到 9之间,多数具有至少7位有效数字。Double 类型的精度通常为 15 到 18之间,多数具有至少16位有效数字。Long double 类型的精度最小为15、18或33位有效数字,取决于它占用多少字节。

我们可以使用名为std::setprecision()输出操纵器(Output manipulators)函数来指定 std::cout 的精度。输出操纵器可以修改数据输出的方式,它们被定义在 iomanip 头文件中.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <iostream>
#include <iomanip> // for output manipulator std::setprecision()

int main()
{
    std::cout << std::setprecision(16); // 显示16位精度
    std::cout << 3.33333333333333333333333333333333333333f <<'\n'; // f 后缀表示 float
    std::cout << 3.33333333333333333333333333333333333333 << '\n'; // 没有后缀默认 double

    return 0;
}

输出结果:

1
2
3.333333253860474
3.333333333333334

通过 std::setprecision()设置精度后,上述数字都被打印为16位精度。但是,如你所见,其精度并没有达到16位。而且由于float的精度比double还低,float类型的值被打印出来后误差也更大。

精度问题不仅仅影响小数,它还会影响具有很多有效数字位的数。让我们考虑如下大数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <iomanip> // for std::setprecision()
#include <iostream>

int main()
{
    float f { 123456789.0f }; // f 有 10 位有效数字
    std::cout << std::setprecision(9); // 显示 f 的 9 位有效数字。
    std::cout << f << '\n';

    return 0;
}

输出

1
123456792

123456792 比 123456789 要大。123456789.0 具有 10 位有效数字,但是float通常只有7位精度(结果123456792正号有7位有效数字)。精度丢失了!精度丢失主要是因为数没有被精确的存储,我们称其为舍入误差(rounding error)

因此,在使用的浮点数精度比对应浮点类型变量能够表示的精度更高时,要多加注意。

最佳实践

如果内存空间允许,尽量使用 double 而不是 float,因为 float 的精度更低,也就有可能造成数值不准确的问题。

舍入误差的存在增加了浮点数比较的难度

浮点数使用起来更加困难,这是因为它的二进制表示法(数据存储形式)和十进制表示法(我们的理解方式)之间的关系并不明显。对于分数 1/10 来说,十进制表示法很容易将其表示为 0.1,而我们也习惯性的认为0.1很容易表示,毕竟它只有一位有效数字。实际上,对于二进制来说,0.1需要表示为一个无穷序列:0.00011001100110011… 因此,当我们把0.1赋值给浮点数时,实际上会出现精度问题。

透过下面程序中你可以看到其影响:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iomanip> // for std::setprecision()
#include <iostream>

int main()
{
    double d{0.1};
    std::cout << d << '\n'; // use default cout precision of 6
    std::cout << std::setprecision(17);
    std::cout << d << '\n';

    return 0;
}

输出结果:

1
2
0.1
0.10000000000000001

第一行,std::cout 打印了 0.1,如我们所料。

第二行,我们让 std::cout 显示17位精度,可以看到,d的打印结果并不是0.1。这是因为double必须对数据进行截断,毕竟内存不是无限的。结果就是数据的精度编程了16位(double保证的),但是其值却不是0.1。舍入误差可能会得到一个稍大一点或稍小一点的值,这取决于截断发生在哪里。

舍入误差会带来非预期的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iomanip> // for std::setprecision()
#include <iostream>

int main()
{
    std::cout << std::setprecision(17);

    double d1{ 1.0 };
    std::cout << d1 << '\n';

    double d2{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 }; // should equal 1.0
    std::cout << d2 << '\n';

    return 0;
}
1
2
1
0.99999999999999989

尽管我们期望 d1 的值应该等于 d2 ,但是实际上我们也看到了它们并不相等。如果我们在程序中对 d1 和 d2 进行比较,那么结果可能就会出人意料。因为浮点数的这种不精确的特性,对浮点数进行比较通常会产生问题——我们会在 5.6 - 关系运算符和浮点数比较中进行详细地讨论。

关于舍入误差的最后一点叮嘱:数学运算 (例如加法和乘法) 会导致舍入误差的积累。所以即使 0.1 只在第 17位有效数字时存在误差,当我们将是个0.1相加时,舍入误差就出现在了第16位。不断地计算会导致这些误差被不断地放大。

关键信息

当数据不能被精确的存储时,会出现舍入误差。即使是非常简单的数字也存在该问题(例如 0.1)。因此,舍入误差总是无时无刻地发生。摄入误差并不是异常,它是规则的一部分。永远不要假设你的浮点数是精确的。

这条规则的结果就是在使用浮点数处理金融或货币数据时,一定要格外警惕。

NaN 和 Inf

还有两类特殊的浮点数。一个是Inf,表示无穷。Inf可以是正也可以是负。第二个是NaN,表示”非数字“。NaN 的种类有很多(我们不会在此讨论)。NaNInf 只有在编译器使用特定格式(IEEE 754) 的浮点数时才可以使用。当使用其他格式的时候,下面的代码就会产生未定义行为

下面的例程展示了上述三种浮点数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>

int main()
{
    double zero {0.0};
    double posinf { 5.0 / zero }; // 正无穷
    std::cout << posinf << '\n';

    double neginf { -5.0 / zero }; // 负无穷
    std::cout << neginf << '\n';

    double nan { zero / zero }; // 非数字 (数学上无效)
    std::cout << nan << '\n';

    return 0;
}

在 Windows 上的 Visual Studio 2008 运行,输出结果如下:

1
2
3
1.#INF
-1.#INF
1.#IND

INF 表示无穷,而 IND表示不确定(indeterminate)。注意,Inf 和 NaN 的打印结果和平台是相关的,在你的电脑上结果可能有所不同。

最佳实践

要彻底避免除0,即使你的的计算机支持这种用法。

结论

总结一下,关于浮点数你需要记住下面两件事:

  1. 浮点数在表示很大或很小的数时非常有用,当然也适合保存有小数部分的数。
  2. 浮点数通常会有微小的舍入误差问题,即便一个数的有效数字比精度能够表示的有效数字少,也存在该问题。很多时候这些问题不会被我们注意到,因为摄入误差很小,而且输出结果会被截断,但是,对浮点数进行比较时,舍入误差的存在可能会带来意外的结果。 对浮点数进行数学运算时也可能造成舍入误差的不断增大。