章节目录

浮点数

本节阅读量:

整数非常适合计算整数,但有时我们需要存储非常大的(正或负)数字,或具有分数的数字。浮点类型变量可以保存具有分数的数字,例如4320.0、-3.33或0.01226。浮点是指小数点可以“浮动”;即,它可以支持小数点之前和之后的可变位数。

有三种不同的浮点数据类型:单精(float)、双精度(double)和长双精(long double)。与整数一样,C++不定义这些类型的实际大小(但保证最小大小)。在现代架构上,浮点数表示几乎总是遵循IEEE754标准格式。在这种格式中,单精度是4个字节,双精度是8,长双精度可以等效于双精度(8个字节)、或80位(通常填充为12个字节)或16个字节。

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

类型 最小大小 常见大小
float 4字节 4字节
double 8字节 8字节
long double 8字节 8,12或16字节

下面是浮点变量的一些定义:

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。默认情况下,如果小数部分为0,则std::cout不会打印数字的小数部分。

在第二种情况下,数字按预期打印。

在第三种情况下,它以科学记数法打印数字。


浮点范围

假设按IEEE 754表示:

位数 范围 精度
4 字节 ±1.18 x 10^-38 to ±3.4 x 10^38 and 0.0 6-9 个有效位, 通常是 7 个
8 字节 ±2.23 x 10^-308 to ±1.80 x 10^308 and 0.0 15-18 个有效位,通常 16 个
80 位 (通常占用12 或 16 字节) ±3.36 x 10^-4932 to ±1.18 x 10^4932 and 0.0 18-21 个有效位
16 字节 ±3.36 x 10^-4932 to ±1.18 x 10^4932 and 0.0 33-36 个有效位

80位浮点类型是因为历史原因。在现代处理器上,它通常使用12或16个字节来实现(处理器处理的更自然的大小)。

80位浮点类型与16字节浮点类型的范围相同,这似乎有点奇怪。这是因为它们具有相同的专用于指数的位数——然而,16字节的数字可以存储更多的有效数字。


浮点精度

考虑分数1/3。这个数字的十进制表示是0.33333333333……其中3向外无穷大。如果你在一张纸上写这个数字,写满整张纸,您所写的数字将接近0.333333333……(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相同。显示的最小指数位数是编译器特定的,Visual Studio使用3,其他一些使用2(根据C99标准)。

浮点变量的精度位数取决于类型(单精度低于双精度)和存储的特定值(某些值的精度高于其他值)。单精度浮点数的精度在6到9位之间,大多数单精度浮点数至少有7个有效数字。双精度值的精度在15到18位之间,大多数双精度值至少有16个有效数字。长双精度数的最小精度为15、18或33个有效数字,具体取决于它占用的字节数。

我们可以通过使用名为std::setprecision()的输出操纵函数来覆盖std::cout显示的默认精度。输出操纵器改变数据的输出方式,并在iomanip头文件中定义。

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

int main()
{
    std::cout << std::setprecision(17); // 输出时,精度保留17位
    std::cout << 3.33333333333333333333333333333333333333f <<'\n'; // f 意味着 float 类型
    std::cout << 3.33333333333333333333333333333333333333 << '\n'; // 没有后缀意味着 double 类型

    return 0;
}

输出:

1
2
3.3333332538604736
3.3333333333333335

因为我们使用std::setprecision() 将精度设置为17位,所以上面的每个数字都用17位数字打印。正如你所看到的,由于单精度数不如双精度数精确,因此单精度数的误差更大。

精度问题不仅仅影响分数,它们还影响任何具有太多有效数字的数字。让我们考虑一个大数字:

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

int main()
{
    float f { 123456789.0f }; // f 有10个有效位
    std::cout << std::setprecision(9); // 输出时,精度保留9位
    std::cout << f << '\n';

    return 0;
}

输出:

1
123456792

123456792大于123456789。值123456789.0有10个有效数字,但浮点值通常有7个精度数字(123456792的结果仅精确到7个有效数字)。我们失去了一些精度!当由于无法精确存储数字而丢失精度时,这称为舍入误差。

因此,当使用需要比变量所能容纳的精度更高的浮点数时,必须小心。


舍入错误使浮点比较棘手

由于二进制(存储数据)和十进制(我们思考所用)数字之间的差异明显,因此浮点数字很难使用。考虑分数1/10。在十进制中,这很容易表示为0.1,我们习惯于将0.1视为具有1个有效数字的易于表示的数字。然而,在二进制中,十进制值0.1由无限序列表示:0.0001100110011……因此,当我们将0.1赋给浮点数时,我们将遇到精度问题。

您可以在以下程序中看到此操作的效果:

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

int main()
{
    double d{0.1};
    std::cout << d << '\n'; // 使用默认输出精度 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 实际上不太精确!这是因为double由于其有限的内存而不得不截断近似。结果是一个精确到16个有效位的数字(double类型保证),但数字不是0.1。舍入误差可能会使数字稍小或稍大,这取决于截断发生的位置。

舍入错误可能会产生意外的后果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iomanip> // 使用默认输出精度 6
#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 }; // 应当等于l 1.0
    std::cout << d2 << '\n';

    return 0;
}

输出

1
2
1
0.99999999999999989

尽管我们可能期望d1和d2应该相等,但我们看到它们并不是。如果我们在程序中比较d1和d2,程序可能不会按预期执行。由于浮点数往往不精确,因此直接比较浮点数通常是有问题的——我们在关系运算符和浮点比较中讨论解决方案。

关于舍入误差的最后一个注意事项:数学运算(如加法和乘法)往往会使舍入误差增加。因此,即使0.1在第17个有效数字中有舍入误差,但当我们将0.1相加10次时,舍入误差已经蔓延到第16个有效数字。继续操作将导致该错误变得越来越严重。


NaN和Inf

浮点数有两种特殊的类别。第一个是Inf,它表示无穷大。Inf可以是正的,也可以是负的。第二个是NaN,它代表“不是数字”。有几种不同类型的NaN(我们在这里不讨论)。NaN和Inf仅在编译器使用特定的浮点数规则(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代表无法确定结果。请注意,打印Inf和NaN的结果是特定于平台的,因此您的结果可能会有所不同。


结论

总之,关于浮点数,应该记住三件事:

  1. 浮点数通常用来存储带有分数,或者非常大或者非常小的数字。
  2. 浮点数通常有小的舍入误差,即使数字的有效位小于精度。大部分情况下,误差比较小,或者输出截断,难以注意到误差。但是比较浮点数的时候,误差就会比较明显。
  3. 在浮点数上执行算数运算会导致误差累积变大。

4.6 科学记数法

上一节

4.8 布尔值(Boolean)

下一节