C语言小知识---奇葩的小数

x33g5p2x  于9个月前 转载在 其他  
字(2.7k)|赞(0)|评价(0)|浏览(83)

看到这个标题,好多人可能会想,小数有什么奇葩的,这不和整数一样,加减乘除计算起来也没啥区别呀?奇葩在哪里?

下面通过一个简单的例子来看一看,定义一个小数一个整数,然后打印出来。

核心代码其实只有3行

float n1=2.1f;
int n2 = 1;
printf( "%f,%d \r\n\r\n\r\n", n1,n2 );

通过串口打印工具观察打印结果

输出的结果也很正常,没有什么奇怪的地方。

下面看看这两个数据在内存中的存储情况

整数1在内存中存储位置从0x000010开始,占两个字节,也就是0x0001。小数2.1在内存中的存储位置从0x00000C开始,占4个字节,也就是0x40066666,看到这个数字,满脑子全是问号???????这是什么鬼?为什么2.1在内存中的存储数据这么大?

同时通过编译器的变量监控也可以看到,2.1实际上的值并不是2.1,而是2.0999999044,那么为什么通过printf()函数打印出来的数字是2.1呢?那是因为printf()函数在打印数据的时候对数据进行了处理。

如果这个例子看起来不直观,那么每次在打印的时候,给这个浮点数加上0.0001看看效果。

可以看到在对2.1003加了0.0001之后,计算的结果已经错误了。那么为什么会出现这种情况呢?这就要从单片机内部对于小数的存储说起。

首先看一下把2.1转换成16进制改如何表示。可以直接去这个 网站上计算

h-schmidt.net/FloatConverter/IEEE754.html

可以看出2.1的16进制值就是0x40066666,和在内存中查看的值是一样的。说明监控的单片机内存中的值是正确的。

那这个值又是来的?这几不得不说C语言对浮点数的存储规定。在C语言中,一个浮点数占32位,其中第一位表示符号位,0表示正数,1表示负数。紧接着8位表示指数部分,也就是系统会将浮点数表示为指数计数法,这8位就是指数部分的值,但是这里的指数是以2为底的。剩下的23位表示的就是尾数,也就是有效数据。

将0x40066666转换为2进制数

最高位0,代表这个数字是正的,然后紧接着8位,就代表指数部分。但是这里的指数部分是以127为偏移的,也就是说这个值减去127就是指数部分的值。这里8位二进制的值是1000 0000 也就是10进制的128,128-127=1,说明指数部分的值是1.也就是2^1。剩下的23位 001100110011001100110 也就是0x066666,表示有效数据转换为10进制就是419430。

在这个网站也可以看到这个计算过程。看这个网站可以有些迷糊,说了这么多,那么具体计算步骤是怎么来的呢?
那么这个2.1是如何转换成二进制呢?

第一步: 将整数部分转换为二进制,整数部分为2,所以2进制就是 10

第二步: 将小数部分转换为二进制, 也就是要将0.1转化为 2a+2b+2c…2n

这个转换要手动计算的话不好实现,可以使用比较简单的方式来实现。
把十进制的小数部分乘以2,取整数部分作为二进制的一位,剩余小数继续乘以2,直至不存在剩余小数为止。
具体实现步骤 如下

0.1 * 2 = 0.2      0
      
      0.2 * 2 = 0.4      0
      0.4 * 2 = 0.8      0
      0.8 * 2 = 1.6      1
      0.6 * 2 = 1.2      1
      
      0.2 * 2 = 0.4      0
      0.4 * 2 = 0.8      0
      0.8 * 2 = 1.6      1
      0.6 * 2 = 1.2      1
      
      0.2 * 2 = 0.4      0
      0.4 * 2 = 0.8      0
      0.8 * 2 = 1.6      1
      0.6 * 2 = 1.2      1
      
      0.2 * 2 = 0.4      0
      0.4 * 2 = 0.8      0
      0.8 * 2 = 1.6      1
      0.6 * 2 = 1.2      1
      
      0.2 * 2 = 0.4      0
      0.4 * 2 = 0.8      0
      0.8 * 2 = 1.6      1
      0.6 * 2 = 1.2      1

可以看到这个是以0011无限循环的一个数字,也就是说小数部分只能无限的接近0.1,而不可能等于0.1.所以这个小数的精度在单片机内部就会丢失。

此时小数部分的二进制数就为 0 0011 0011 0011 0011 0011

接下来就将整数的和小数组合起来。整数部分10 加上小数部分就是 10.0 0011 0011 0011 0011 0011

下面换算指数,整数2用指数表示的话就是2^1,也就是指数部分最大值是1。接下来将数字转换为指数表示形式。

1.00 0011 0011 0011 0011 0011 /* 2^1 小数点向前移动一位,让整数部分为1,后面乘以2^1。

第三步:将指数部分换算为二进制。

指数部分为1,但是不能直接写为 0000 0001,这是因为在小数里面,最左边是最高位,最右边是最低位。也就是说左边位是1/21,最右边是1/28。所以再计算指数的二进制值时,要给计算的数据加上127,然后在换算成二进制。

1+127=128,128的二进制数就是 1000 0000

第四步:组合指数和有效数据

指数为 1000 0000 有效数据为 1.00 0011 0011 0011 0011 0011

在组合的时候,要将有效数据的整数部分的1去掉,因为换算为二进制指数表示后,整数部分永远是1。所以组合后的数据为 1000 0000 .00 0011 0011 0011 0011 0011

第五步:添加符号位

最高位是符号位,0表示数字为正数,1表示数字为负数。2.1为正数,所以最高位数字就是0.

将所有的数据组合起来就是 0 1000 0000 .00 0011 0011 0011 0011 0011 去掉小数点,整理后的数据为

0100 0000 0000 0110 0110 0110 0110 0110

1位符号位,8位指数位,23位有效数据位,如果有效数据不足23位,必须要补足23位。

这个而二进制数转换为16进制数字就是 0x40066666,和计算机内部存储的数据一样。

看到这里也许就明白了,浮点数在存储的时候,精度已经就丢失了。所以在计算的过程中如何需要不停地存储和计算,那么精度丢失的会更加厉害。这也就解释了上面演示的程序,为什么计算到第四步的时候,才会出现计算错误。

这时在看网上的段子 有人说 0.1 +0.2 不等于0.3被人骂傻x,也许你就会会心一笑,因为0.1+0.2真的不等于0.3,它等于 0.300000012。

小数这么奇葩,难道以后写程序时不能用小数吗?当然不是的,在程序中有小数计算时,为了不丢失计算的精度,一般都会先把小数扩大一定的倍数变成整数,然后进行计算,计算完成后再将整数缩小一定的倍数,在变回小数。

上面测试的程序修改如下:

因为累加的数字是0.0001,所以就将数据统一扩大10000倍,这样累加的时候,就可以直接累加1,累加完成后需要打印的时候,再将数据缩小10000倍,这样打印的数据结果就是正确的了。

相关文章