浮点数
本节阅读量:整数非常适合计算整数,但有时我们需要存储非常大(正或负)的数字,或者带有小数部分的数字。浮点类型变量可以保存带有小数部分的数字,例如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字节 |
下面是浮点变量的一些定义:
|
|
输入浮点数时,始终至少包含一位小数(即使小数部分为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位有效数字(float的最小精度),超出部分将被截断。
以下程序显示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个有效数字,但float类型通常只有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,代表”不是数字”(Not a Number)。NaN有几种不同的类型(这里不展开讨论)。NaN和Inf仅在编译器使用特定的浮点数规则(IEEE 754)时才可用。如果编译器使用其他标准,则以下代码将产生未定义行为。
具体见下面的示例:
|
|
在Windows上使用Visual Studio 2008运行的结果如下:
|
|
INF代表无穷大,IND代表无法确定的结果。请注意,打印Inf和NaN的结果因平台而异,因此您的输出可能会有所不同。
最佳实践
完全避免除以0.0,即使编译器支持它。
结论
总之,关于浮点数,应该记住三件事:
- 浮点数通常用来存储带有小数部分的数字,或者非常大、非常小的数字。
- 浮点数通常会有微小的舍入误差,即使数字的有效位数在精度范围内。大部分情况下,误差很小或者被输出截断,难以察觉。但在比较浮点数时,误差就会变得比较明显。
- 对浮点数执行算术运算会导致舍入误差不断累积。