flaot 数据类型的一些坑(大数吃小数)

首先我们来看一段代码,你认为它会输出什么呢?

#include<stdlib.h>
int main()
{
    int i = 0;
    float j = 1.0;
    float sum =0;
    for(i = 0 ; i < 20000000 ; i ++)
        sum += j;
    printf("%f\n",sum);
}

 解析:逻辑上就是将1.0进行累加2千万次。我们预计的结果应该是20000000。
但是结果却如图:

在这里插入图片描述

毫无疑问,这肯定和float数据类型有关,但是至于为什么会出现这个问题,我们一起来分析。

float数据类型如何表示
我们一般都了解int数据类型是如何表示的:它是由32bit位组成,若是有符号int,最高位是符号位,低31位表示有效范围。无符号int ,32位都是表示有效范围(32位操作系统,本章中所有demo或话术都是基于32位操作系统)。

但是对float数据类型的表示,我相信大多数人都不太了解。于是今天我们一起来深入学习,了解在工作或学习中需要注意的事项。

float数据类型在内存中的格式如图所示:

float数据类型格式

第一部分是符号位,用s表示,用来表示符号位。float类型和int类型不一样,int类型可以通过unsigned修饰来表示有符号或无符号数据。float类型都是有符号的。

第二部分是8bit的指数位用e表示,我们用1 ~ 254映射到-126 ~ 127这254个有正有负的数上,因为浮点数不仅要表示很大的数,也要表示很小的数,因此,指数位也应该有负值。

第三部分是23位的有效位,用f表示。
用科学表示法,浮点数可以表示如下:
在这里插入图片描述

从该表达式中,我们无法表示数据0。因此我们就需要一些约定,来表示一些特殊的数。如图:

在这里插入图片描述

当e=0,并且f=0时,浮点数就表示0。

例子

我们一起以0.5为例进行分析:

在这里插入图片描述

因此,s=0,f=0,e=-1。而e是用1 ~ 254映射-126 ~ 127。因而,e在内存中的应该是126。即0.5在内存中的表示应为下图:

在这里插入图片描述

精度损失问题
通过浮点数的数据格式我们知道,有效位是23位,因此对于有些数保存在计算机中就会出现精度损失的情况。比如9.1。
9.1=1001.000110011(以0011循环)…转换为二进制科学表示法就是9.1=1.001000110011(以0011循环)…x 2 3 2^32 
3
 。因此,s=0,e=3,f=001000110011(以0011循环)。由于f只有23bit,所以就会存在精度缺失。如图:
在这里插入图片描述

至此,9.1保存在内存中的二进制为010000010 0010 0011001100110011 001,在转换为十进制就是9.09999942779541015625。

这就解释了为什么0.3+0.6=0.899999。因为0.3转换为浮点数保存到内存中后,不再是准确的0.3了。0.6也是如此。(有些平台demo打印出来的是0.900000,那是因为默认打印位数问题,可通过printf("%1.10f",sum)控制小数点位数。并且不同的编译器精度缺失的处理方式不同,我在ubuntu 18的测试环境中,得到的值是大于0.9的)

大数吃小数
上面介绍了浮点数精度损失的问题,我们再来看一下大数吃小数的问题。还是直接来上demo:
在这里插入图片描述

实际输出为:

在这里插入图片描述

从现象上看就是一个很大的浮点数和一个较小的浮点数之和,得到的还是较大的浮点数(大数吃小数)。原因是为何呢?我们一起探究一下。

其实这就是浮点数加法计算的原理过程分析,核心就是先对齐再计算。

例:0.5 + 0.125
分析:
0.5 = ( − 1 ) 0 (-1)^0(−1) 
0
  x 1.0 x 2 − 1 2 ^{-1}2 
−1
 
0.125 = ( − 1 ) 0 (-1)^0(−1) 
0
  x 1.0 x 2 − 3 2^{-3}2 
−3
 
由于0.5和0.125的指数位不相等(-1和-3)需要先对齐(统一为较大的指数位),即:
0.125 = ( − 1 ) 0 (-1)^0(−1) 
0
  x 0.01 x 2 − 1 2^{-1}2 
−1
  (指数位对齐,对应的有效位就要右移)

0.5 + 0.125 = ( − 1 ) 0 (-1)^0(−1) 
0
  x 1.0 x 2 − 1 2 ^{-1}2 
−1
  + ( − 1 ) 0 (-1)^0(−1) 
0
  x 0.01 x 2 − 1 2^{-1}2 
−1
  = ( − 1 ) 0 (-1)^0(−1) 
0
  x 1.01 x 2 − 1 2^{-1}2 
−1
 

上述就是浮点数求和的过程。我相信,大家应该已经知道大数吃小数的原因了。由于有效位是23位,当较大数是较小数的 2 24 2^{24}2 
24
  (16777216‬)倍之多时,较小数为了将指数位对齐,有效位就会右移很多位,导致有效位中整数部分在23bit之后。科学表达式为:( − 1 ) 0 (-1)^0(−1) 
0
  x 0.0 x 2 n 2 ^{n}2 
n
 =0,这也就解释了开篇的问题。

当浮点数1.0进行累加时,sum为16777216时,是1.0的2 24 2^{24}2 
24
 倍,之后的累加就出现了大数吃小数。
输出为:16777216

思考
通过上面的分享,我们在使用浮点数时,要尽量避免两个坑:精度缺失和大数吃小数。以下是结合自己的经验做的总结:

工作中尽量减少对浮点型数据的使用
曾经我们的研发总监明确要求我们编码中不准使用浮点数,主要是因为我们的业务一般不会用到浮点数。我相信很多公司也要求尽可能少的使用浮点数。但是有些业务需求一定会用到小数,比如:商场中商品的价格表,或者是银行中账户金额。我们可以通过字符串的方式保存这些值(虽然有点浪费内存),就可以避免精度缺失的问题

面试中遇到的问题
曾经我在面试的时候,面试官让我对下面的问题进行编程:

例:计算代数式 1+1 2 \frac{1}{2} 
2
1
​    
 +1 3 \frac{1}{3} 
3
1
​    
 +1 4 \frac{1}{4} 
4
1
​    
 +…+1 n \frac{1}{n} 
n
1
​    
 的值
我的解法如下:
#include<stdio.h>
int main()
{
    long i,n;
    double sum;
    scanf("%ld",&n);
    sum=0.0;
    for(i=1;i<=n;i++)
           sum+=1.0/i;
    printf("%.12f\n",sum); 
    return 0;
 }
 

 其实这不是正确的解,应该将for循环改为for(i=n;i>=1;i--)。这点你get到了吗?

————————————————
版权声明:本文为CSDN博主「quectel-wifi」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xieyihua1994/article/details/106137932

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值