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。浮点数名字中的浮点二字,形象地说明了小数点可以浮动;也就是说它可以支持小数点前和小数点后具有不同位的数字。
浮点数数据类型有三种:float, double 和 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 |
|
在使用浮点数字面量时,请始终保留一位小数(即使这一位是0)。这可以帮助编译器明确该数值为浮点数而非整型数。
1 2 3 |
|
注意,默认情况下浮点数字面量为double
。使用f
后缀可以标注该字面量为float
最佳实践
请确保你使用的字面量和它赋值的类型是匹配的。否则会发生不必要的转换,导致精度丢失。
注意
请确保在应当使用浮点数字面量时,不要误用整型字面量。这可能会发生对浮点类型对象初始化、赋值、数学运算以及调用返回值应当为浮点数的函数时。
打印浮点数
考虑下面这个简单的程序:
1 2 3 4 5 6 7 8 9 10 |
|
上述程序的输出结果可能会让你感到意外:
1 2 3 |
|
在第一条打印中,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 |
|
输出结果为:
1 2 3 4 5 |
|
我们注意到,输出结果中的数都只有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 |
|
输出结果:
1 2 |
|
通过 std::setprecision()
设置精度后,上述数字都被打印为16位精度。但是,如你所见,其精度并没有达到16位。而且由于float
的精度比double
还低,float
类型的值被打印出来后误差也更大。
精度问题不仅仅影响小数,它还会影响具有很多有效数字位的数。让我们考虑如下大数:
1 2 3 4 5 6 7 8 9 10 11 |
|
输出
1 |
|
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 |
|
输出结果:
1 2 |
|
第一行,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 |
|
1 2 |
|
尽管我们期望 d1 的值应该等于 d2 ,但是实际上我们也看到了它们并不相等。如果我们在程序中对 d1 和 d2 进行比较,那么结果可能就会出人意料。因为浮点数的这种不精确的特性,对浮点数进行比较通常会产生问题——我们会在 5.6 - 关系运算符和浮点数比较中进行详细地讨论。
关于舍入误差的最后一点叮嘱:数学运算 (例如加法和乘法) 会导致舍入误差的积累。所以即使 0.1 只在第 17位有效数字时存在误差,当我们将是个0.1相加时,舍入误差就出现在了第16位。不断地计算会导致这些误差被不断地放大。
关键信息
当数据不能被精确的存储时,会出现舍入误差。即使是非常简单的数字也存在该问题(例如 0.1)。因此,舍入误差总是无时无刻地发生。摄入误差并不是异常,它是规则的一部分。永远不要假设你的浮点数是精确的。
这条规则的结果就是在使用浮点数处理金融或货币数据时,一定要格外警惕。
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 |
|
在 Windows 上的 Visual Studio 2008 运行,输出结果如下:
1 2 3 |
|
INF
表示无穷,而 IND
表示不确定(indeterminate)。注意,Inf
和 NaN
的打印结果和平台是相关的,在你的电脑上结果可能有所不同。
最佳实践
要彻底避免除0,即使你的的计算机支持这种用法。
结论
总结一下,关于浮点数你需要记住下面两件事:
- 浮点数在表示很大或很小的数时非常有用,当然也适合保存有小数部分的数。
- 浮点数通常会有微小的舍入误差问题,即便一个数的有效数字比精度能够表示的有效数字少,也存在该问题。很多时候这些问题不会被我们注意到,因为摄入误差很小,而且输出结果会被截断,但是,对浮点数进行比较时,舍入误差的存在可能会带来意外的结果。 对浮点数进行数学运算时也可能造成舍入误差的不断增大。