章节目录

浮点数

本节阅读量:

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

有三种不同的浮点数据类型:单精度(float)、双精度(double)和长双精度(long double)。与整数一样,C++不定义这些类型的实际大小(但保证最小大小)。在现代架构上,浮点数的表示几乎总是遵循IEEE 754标准格式。在该格式中,单精度占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位有效数字(float的最小精度),超出部分将被截断。

以下程序显示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个有效数字,但float类型通常只有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,代表”不是数字”(Not a Number)。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)

下一节