从如何判断浮点数是否等于0说起——浮点数的机器级表示

  朋友在谈一个物流相关的项目,是以前项目的一个延续,涉及到后台的扩展,手机端的App,外加两个App的对接的蓝牙打印机。这个项目前后说了一个多月了吧,最近才草拟了协议。项目本来不复杂,但是客户却如此的拖延。我觉得客户做事好慢,而朋友觉得是自己的就是自己的,不是自己的急也没有用。不断的打电话询问客户,可能最后还被压价,反而更没办法做了。他其实比我还急,但是人家的心态好。的确凡事急不得。

题目中针对的0,对于浮点类型,具体指的是0.0,自然对于指针类型就是NULL,对于整型就是0,一些常见笔试面试题中常出现,不要较真,十分欢迎提出改进意见。

 

本文很大程度上收到林锐博士一些文章的启发,lz也是在大学期间读过,感觉收益良多,但是当时林锐也是说了结论,lz也只是知其然,而不知其所以然,为什么要那样写?为什么要这样用?往往一深究起来就稀里糊涂了,现在有幸还是继续读书,我发现了很多问题理解的还不透彻,亡羊补牢。

浮点数

比如:有int d;  int *d; bool d; double
d;几个变量,经过一系列的计算之后,那么去判断这个四个变量是否等于0该怎么做?

  在 C 语言中,有两种存储浮点数的方式,分别是 float 和 double
,当然了还有long
double。这几种浮点型所容纳的长度不同,当然它们存储的精度也就不同了。

很多菜鸟或者编程功底不扎实的就会出错,一些烂书,尤其国内的一部分大学教材,教授编程语言的书籍,比如谭xx的,都存在很多不规范的误导,甚至是错误,这样的地方简直太多了,并不是程序出了想要的正确结果,就算完事儿了。

  对于整形而言,比如 int 、short 、char
之类的,在内存中的存储方式都是用 补码
进行表示。而浮点数在内存中并没有使用补码进行表示。浮点数在内存中存储的方式使用了
IEEE 的编码表示方式,即使用 符号指数 和
尾数 的形式进行存储的。

一些类似我这样的读过几本经典书籍,看过一些经典技术手册,码过若干行的代码等等,就会说这还不简单,会类似的写出:

 

 1     void isZero(double d)
 2     {
 3         if (d >= -DBL_EPSILON && d <= DBL_EPSILON)
 4         {
 5             //d是0处理
 6         }
 7     }
 8 
 9     void isZero(int d)
10     {
11         if (0 == d)
12         {
13             //d是0处理
14         }
15     }
16 
17     void isZero(int *d)
18     {
19         if (NULL == d)
20         {
21             //d是空指针处理
22         }
23     }
24 
25     void isZero(bool d)
26     {
27         if (!d)
28         {
29             //d就认为是false 也就是0
30         }
31     }

IEEE浮点数表示

没错,很多经典的教科书或者指南,一些技术类的讲义,都会这样教授。但是为什么要这样写?

  用 IEEE 编码表示浮点数,需要 3 部分进行表示,分别是
符号指数 和 尾数。符号位占用 1 位,0 表示正数,1
表示负数。指数 和 尾数 根据 float 和 double 类型的不同而长度不同。

可能一部分人就糊涂了,不知道咋回答,搞技术或者做学问不是诗词歌赋,结论经不起严谨的推敲就不能服众,不可以说,书上是这样写的,或者老师告诉我的,那样太low了。尤其是浮点数比较的问题,不只是0,类似的和其他的浮点数比较大小的问题也是一样的。

  

要解决这个疑惑,必须先理解计算机是如何表示和存储浮点数据的,期间参考了IEEE单双精度的规范文档,和MSDN的一些文档,以及《深入理解计算机操作系统》一书。

  IEEE 二进制浮点数的表示:

1、先看看双精度的伊布西龙(高等数学或者初等数学里的数学符号就是它,epsilon)的值是多少

位数  符号位  指数位  尾数位
32     1            8           23     单精度(float)
64     1           11          52     双精度(double)

printf("%.40lf", DBL_EPSILON);

 

图片 1

编码转换

折合为科学计数法:图片 2

以单精度为例:把3.75用IEEE表示法表示

2、再看一些例子

1、把 10 进制转换为2进制:3.75D=11.11B

    printf("%0.100f\n", 2.7);
    printf("%0.100f\n", 0.2);

2、 尾数正规化                     1.111*2^1

图片 3

3、 修正指数                         1+127=128 1000 0000

 printf("%0.100f\n", sin(3.141592653589793 / 6));

4、 符号 0表示正,1表示负

这个计算结果不是0.5,而是:

5、 IEEE表示                         0 1000 0000 1110 0000 0000 0000
0000 000

图片 4

6、 转换为16进制:              0100 0000 0111 0000 0000 0000 0000 0000
  40 70 00 00 

printf("%0.100f\n", 0.0000001);

 

打印结果是:

用 C 程序进行验证

图片 5

  写一个简单的 C 程序来验证上面的转换,代码如下:

这样的结果在不同机器或者编译器下,有可能不同,但是能说明一个问题,浮点数的比较,不能简单的使用==,而科学的做法是依靠EPISILON,这个比较小的正数(英文单词episilon的中文解释)。

 1 #include <stdio.h>
 2 
 3 int main()
 4 {
 5     float f = 3.75f;
 6 
 7     printf("%f \r\n", f);
 8 
 9     return 0;
10 }

EPSILON被规定为是最小误差,换句话说就是使得EPSILON+1.0不等于1.0的最小的正数,也就是如果正数d小于EPISILON,那么d和1.0相加,计算机就认为还是等于1.0,这个EPISILON是变和不变的临界值。

  以上代码用 VS 2012 编译,调试运行查看内存,如下图所示。

官方解释:

图片 6

For EPSILON, you can use the
constants FLT_EPSILON, which is defined for float as 1.192092896e-07F,
or DBL_EPSILON, which is defined for double as 2.2204460492503131e-016.
You need to include float.h for these constants. These constants are
defined as the smallest positive number x, such that x+1.0 is not equal
to 1.0. Because this is a very small number, you should employ
user-defined tolerance for calculations involving very large
numbers.

  图中的00 00 70 40是以小尾方式存储的,其值为40 70 00
00,与我们手动转换的值相同。

一般可以这样写,防止出错:

 

 1     double dd = sin(3.141592653589793 / 6);
 2     /*if (dd == 0.5)
 3     {取决于不同的编译器或者机器平台……这样写,即使有时候是对的,但是就怕习惯,很容易出错。
 4     }*/
 5 
 6     if (fabs(dd - 0.5) < DBL_EPSILON)
 7     {
 8         //满足这个条件,我们就认为dd和0.5相等,否则不等
 9         puts("ok");//打印了ok
10     }

为什么浮点数的表示是不精确的?(简单的分析,否则里面的东西太多了)

这得先说说IEEE(Institute of Electrical and Electronic
Engineers )754标准,此标准规定了标准浮点数的格式,目前,几乎所有计算机都支持该标准,这大大改善了科学应用程序的可移植性。下面看看浮点数的表示格式:n是浮点数,s是符号位,m是尾数,e是阶数,回忆高中的指数表示。

图片 7           
 图片 8

IEEE标准754规定了三种浮点数格式:单精度、双精度、扩展精度。

前两者正好对应C、C++的float、double,其中,单精度是32位,S是符号位,占1位,E是阶码,占8位,M是尾数,占23位,双精度是64位,其中S占1位,E占11位,M占52位。拿intel架构下的32位机器说话,之前在计算机存储的大小端模式解析说过处理器的两类存储方式,intel处理器是小端模式,为了简单说明,以单精度的20000.4为例子。

20000.4转换为单精度的2进制是多少?

此单精度浮点数是正数,那么尾数符号s=0,指数(阶数)e是8位,30到23位,尾数m(科学计数法的小数部分)23位长,22位到0位,共32位,如图

图片 9

先看整数部分,20000先化为16进制(4e20)16,则二进制是(100 1110 0010
0000)2,一共15位。

再看小数部分,0.4化为二进制数,这里使用乘权值取整的计算方法,使用0.X循环乘2,每次取整数部分,但是我们发现,无论如何x2,都很难使得0.X为0.0,就相当于十进制的无限循环小数0.33333……一样,10进制数,无法精确的表达三分之一。也就是人们说的所谓的浮点数精度问题。因单精度浮点数的尾数规定长23位,那现在乘下去,凑够24位为止,即再续9位是(1.011001100)2


这里解释下为什么是1. ……  且 尾数需要凑够24位,而不是23位?

尾数M,单精度23位、双精度52位,但只表示小数点之后的二进制位数,也就是假定M为
“010110011…” , 二进制是 “ . 010110011…” 。而IEEE标准规定,小数点左边还有一个隐含位,这个隐含位绝大多数情况下是1,当浮点数非常非常非常小的时候,比如小于
2^(-126)
(单精度)的时候隐含位是0。这个尾数的隐含位等价于一位精度,于是M最后结果可能是”1.010110011…”或“0.010110011…”。也就是说尾数的这个隐含位占了一位精度!且尾数的隐含位这一位并不存放在内存里。


发表评论

电子邮件地址不会被公开。 必填项已用*标注