浮点数
本节阅读量:整数非常适合计算整数,但有时我们需要存储非常大的(正或负)数字,或具有分数的数字。浮点类型变量可以保存具有分数的数字,例如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字节 |
下面是浮点变量的一些定义:
|
|
输入浮点数时,始终至少包含一个小数位(即使小数为0)。这有助于编译器理解数字是浮点数,而不是整数。
|
|
请注意,默认情况下,浮点字面值默认为类型double。f后缀用于表示float类型的文本。
最佳实践
始终确保字面值的类型与它们用于初始化的变量的类型匹配。否则,将导致不必要的转换,可能会丢失精度。
打印浮点数字
现在考虑这个简单的程序:
|
|
这个看似简单的程序的结果可能会让您惊讶:
|
|
在第一种情况下,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位:
|
|
该程序输出:
|
|
请注意,每个都只有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头文件中定义。
|
|
输出:
|
|
因为我们使用std::setprecision() 将精度设置为17位,所以上面的每个数字都用17位数字打印。正如你所看到的,由于单精度数不如双精度数精确,因此单精度数的误差更大。
精度问题不仅仅影响分数,它们还影响任何具有太多有效数字的数字。让我们考虑一个大数字:
|
|
输出:
|
|
123456792大于123456789。值123456789.0有10个有效数字,但浮点值通常有7个精度数字(123456792的结果仅精确到7个有效数字)。我们失去了一些精度!当由于无法精确存储数字而丢失精度时,这称为舍入误差。
因此,当使用需要比变量所能容纳的精度更高的浮点数时,必须小心。
提示
输出格式操纵器(和输入格式操纵器)是粘性的——这意味着如果设置它们,下次修改之前它们将保持设置的状态。
最佳实践
除非存储空间紧张,否则使用双精度浮点,因为单精度浮点数通常会导致保存的不准确。
舍入错误使浮点比较棘手
由于二进制(存储数据)和十进制(我们思考所用)数字之间的差异明显,因此浮点数字很难使用。考虑分数1/10。在十进制中,这很容易表示为0.1,我们习惯于将0.1视为具有1个有效数字的易于表示的数字。然而,在二进制中,十进制值0.1由无限序列表示:0.0001100110011……因此,当我们将0.1赋给浮点数时,我们将遇到精度问题。
您可以在以下程序中看到此操作的效果:
|
|
输出:
|
|
在首行,std::cout按预期打印0.1。
在第二行上,std::cout向我们显示17位精度,我们看到 d 实际上不太精确!这是因为double由于其有限的内存而不得不截断近似。结果是一个精确到16个有效位的数字(double类型保证),但数字不是0.1。舍入误差可能会使数字稍小或稍大,这取决于截断发生的位置。
舍入错误可能会产生意外的后果:
|
|
输出
|
|
尽管我们可能期望d1和d2应该相等,但我们看到它们并不是。如果我们在程序中比较d1和d2,程序可能不会按预期执行。由于浮点数往往不精确,因此直接比较浮点数通常是有问题的——我们在关系运算符和浮点比较中讨论解决方案。
关于舍入误差的最后一个注意事项:数学运算(如加法和乘法)往往会使舍入误差增加。因此,即使0.1在第17个有效数字中有舍入误差,但当我们将0.1相加10次时,舍入误差已经蔓延到第16个有效数字。继续操作将导致该错误变得越来越严重。
关键点
当无法精确存储数字时,会发生舍入错误。即使是简单的数字,如0.1,也可能发生这种情况。因此,舍入误差可以并且确实会随时发生。舍入误差并不例外——它们是规则。永远不要假设浮点数是准确的。
这条规则的一个推论是:对金融或货币数据使用浮点数要谨慎。
NaN和Inf
浮点数有两种特殊的类别。第一个是Inf,它表示无穷大。Inf可以是正的,也可以是负的。第二个是NaN,它代表“不是数字”。有几种不同类型的NaN(我们在这里不讨论)。NaN和Inf仅在编译器使用特定的浮点数规则(IEEE 754)时可用。如果编译器使用其他标准,则以下代码将产生未定义的行为。
具体见下面的示例:
|
|
在Windows上使用Visual Studio 2008运行的结果:
|
|
INF代表无穷大,IND代表无法确定结果。请注意,打印Inf和NaN的结果是特定于平台的,因此您的结果可能会有所不同。
最佳实践
完全避免除以0.0,即使编译器支持它。
结论
总之,关于浮点数,应该记住三件事:
- 浮点数通常用来存储带有分数,或者非常大或者非常小的数字。
- 浮点数通常有小的舍入误差,即使数字的有效位小于精度。大部分情况下,误差比较小,或者输出截断,难以注意到误差。但是比较浮点数的时候,误差就会比较明显。
- 在浮点数上执行算数运算会导致误差累积变大。
