06. 小数和小数精度丢失

25 阅读1分钟

1. 将0.1累加100次也得不到10

#include <stdio.h>

int main()
{
    float sum = 0;    
	// 0.1相加100次
    for (int i = 1;  i <= 100; i++) {
		sum += 0.1;    
	}   
	// 显示结果
    printf("%f\n", sum); 
}

打印结果:

10.000002

以上代码没有错,计算机也没有故障,为什么会出现这样的情况?

2. 用二进制表示小数

二进制数:1011.0011 如何换算为十进制?

如下:

3. 计算机出错的原因

小数点后4位能够用二进制数表示的数值二进制数是连续的,十进制数是非连贯的

二进制数十进制数
0.00000
0.00010.0625
0.00100.125
0.00110.1875
0.01000.25
0.01010.3125
0.01100.375
0.01110.4375

上面发现通过二进制数无法表示0.1,0.0625直接就跳到了0.125,根本就没有0.1这个数,实际上十进制数0.1转换成二进制后,会变成0.00011001100…(1100循环)这样的循环小数。

这和无法用十进制数来表示1/3是一样的道理。1/3就是0.3333…,同样是循环小数。

计算机就会根据变量数据类型所对应的长度将数值从中间截断或者四舍五入,就像0.3333.... 从中间截断会变成0.333333,它的三倍是无法得出1的,结果是0.999999。 所以0.1的100倍的结果是10.000002。

4. 什么是浮点数

实际上像1011.0011这样带小数点的表现形式在计算机中并非上述的表现形式,那么在计算机内部机是以什么样的表现形式来处理小数的呢?

浮点数分为单精度浮点数和双精度浮点数。双精度浮点数类型用64位、单精度浮点数类型用32位来表示。分别是double和float。

那么浮点数究竟是什么样子的?浮点数是通过尾数和指数来表示的,也就是科学记数法

± m * 2^n^

  • ±:符号
  • m:尾数
  • 2:基数
  • n:指数

那么具体是怎么存储浮点数据的,这里的指数位和尾数位又是怎么表现的。

我们先看看10进制的数用尾数和指数的表现形式。比如:

可以看出通过尾数和指数有多种表现形式。

再来看看314.15的二进制100111010.0010011001100110011(无限循环)数用尾数和指数的表现形式

通过尾数和指数有多种表现形式,实际上浮点数尾数部分用的是“将小数点前面的值固定为1的表达式”,并且只保留小数点后面的部分。如上就是:314.15的尾数部分就是 1.001110100010011001100110011 * 2^8^ 中的 001110100010011001100110011(去掉小数点前面的1),保留23位就是00111010001001100110011,

再来看指数部分,指数部分中使用的EXCESS系统,使用这种方法主要是为了表示负数时不使用符号位。EXCESS系统表现是指,通过将指数部分表示范围的中间值设为0,使得负数不需要用符号来表示。也就是说,当指数部分是8位单精度浮点数时,最大值11111111 = 255的1/2,即01111111 = 127(小数部分舍弃)表示的是0,指数部分是11位双精度浮点数时,11111111111= 2047的1/2,即01111111111 = 1023(小数部分舍弃)表示的是0。

二进制十进制EXCESS表现形式
000000000-127(0 - 127)
000000011-126(1 - 127)
.........
01111110126-1(126 - 127)
011111111270(127 - 127)
.........
11111111255128(255 - 127)
  • 指数位的存储是在原有数值上+127。
  • 指数位在使用的时候再在存储的值上-127。

上述 1.001110100010011001100110011 * 2^8^ 的指数部分的值是8,转为二进制就是1000,加上127就等于135,转为二进制数就是10000111。即314.15的指数部分是10000111

所以单精度浮点数314.15在计算机的存储是 0 10000111 00111010001001100110011

  • 符号位:0
  • 指数位:10000111
  • 尾数位:00111010001001100110011

再来逆向算一遍:

  • 指数位:10000111 转为 10进制就是 135,减去127就等于 8。
  • 尾数位:00111010001001100110011前面加1.就是 1.00111010001001100110011 转为十进制就是 1.227148413658142。
  • 结果就是 1.227148413658142 * 2^8^ = 1.227148413658142 * 256 ≈ 314.15

5. 总结

浮点数是计算机内部用尾数和指数来表示的,采用科学记数法的形式,即 ±m * 2^n,其中符号位、指数位和尾数位占据不同的位数,分别存储不同的数据。浮点数在表示小数时会产生精度误差,因为某些十进制数在二进制下是无限循环小数,而计算机二进制又有精度限制,无法精确存储和表示这些无限循环小数,所以在进行浮点数运算时可能会出现误差。如果需要高精度运算,需要使用专门的高精度库。

关注微信公众号:“小虎哥的技术博客”,让我们一起成为更优秀的程序员❤️!

文章和代码仓库:

gitee(推荐):gitee.com/cunzaizhe/x…

github:github.com/tigerleeli/…