C语言的本质(4)——浮点数的本质与运算

C语言规定了3种浮点数,float型、double型和long double型,其中float型占4个字节,double型占8个字节,longdouble型长度要大于等于double型,本文档将以float型为例进行介绍,double型和long double型只是比float型位数长,原理都是一样的。

float型可以表示的范围是-3.402823466e38~3.402823466e38,而作为同为4个字节的定点数却只能表示-2147483648~2147483647的范围,使用同样的内存空间,浮点数却能比定点数表示大得多的范围,这是不是太神奇了?既然浮点数能表示这么大的范围,那么我们为何不使用浮点数来代替定点数呢?

先不说浮点数实现起来比较复杂,有些处理器还专门配置了硬件浮点运算单元用于浮点运算,主要原因是浮点数根本就无法取代定点数,因为精度问题。鱼和熊掌不可兼得,浮点数表示了非常大的范围,但它失去了非常准的精度。在说明精度问题前,我们先了解一下浮点数的格式。

ANSI/IEEEStd 754-1985标准

IEEE 754是最广泛使用的二进制浮点数算术标准,被许多CPU与浮点运算器所采用。IEEE754规定了多种表示浮点数值的方式,在本文档里只介绍32bits的float浮点类型。它被分为3个部分,分别是符号位S(sign bit)、指数偏差E(exponent bias)和小数部分F(fraction)。

其中S位占1bit,为bit31。S位为0代表浮点数是正数,S位为1代表浮点数是负数,比如说0x449A522C的S位为0,表示这是一个正数,0x849A522C的S位为1,表示这是一个负数。

E位占8bits,为bit23~bit30。E位代表2的N次方,但需要减去127,比如说E位为87,那么E位的值为2(87-127)=9.094947017729282379150390625e-13。

F位占23bits,为bit0~bit22。F位是小数点后面的位数,其中bit22是2-1=0.5,bit21是2-2=0.25,以此类推,bit0为2-23=0.00000011920928955078125。但F位里隐藏了一个1,也就是说F位所表示的值是1+(F位bit22~bit0所表示的数值),比如说F位是0b10100000000000000000001,只有bit22、bit20和bit0为1,那么F位的值为1+(2-1+2-3+2-23),为1.62500011920928955078125。

综上所述,从二进制数换算到浮点数的公式为:(-1)S×2E-127×(1+F)。但还有几个特殊的情形:

1、若E位为0并且F位也为0时表示浮点数0,此时浮点数受S位影响,表现出+0和-0两种0,但数值是相等的。比如二进制数0x00000000表示+0,二进制数0x80000000表示-0。

2、若E位为0并且F位不为0时浮点数为(-1)S×2-126×F,注意,E位的指数是-126,而不是0-127=-127,而且F位是0.xx格式而不是1.xx格式,比如0x00000001的浮点数为2-126×2-23=1.4012984643248170709237295832899e-45,而不是20-121×(1+2-23)。一旦E为不为0,从0变为1,不是增加2倍的关系,因为公式改变了。

3、若E位为255并且F位不为0时表示非数值,也就是说是非法数,例如0x7F800001。

4、 若E位为255并且F位为0时表示无穷大的数,此时浮点数受S位影响,例如0x7F800000表示正无穷大,0xFF800000表示负无穷大。当我们使用1个数除以0时,结果将被记作0x7F800000。

浮点型在多个处理器间通信时,传递的数值是它的二进制数,比如说1234.5678这个浮点数的二进制数是0x449A522B,如果使用串口发送的话,就会发现串口里发送的是0x44、0x9A、0x52和0x2B这4个数(发送的顺序也可能是逆序,这与约定的字节序有关,与浮点格式无关),接收端接收到这4个数字后再组合成0x449A522B,按照IEEE 754的定义被解析成1234.5678,这样就实现浮点数通信了。如果两个处理器所使用的浮点数规则不同,则无法传递浮点数。

浮点数的换算

下面来看看浮点数与二进制数如何转换。

1、二进制数换算成浮点数:

假如在内存中有一个二进制数为0x449A522C,先将十六进制转换成二进制,如下:

0100 0100  1001  1010 0101  0010  0010 1100

按照SEF的格式分段,如下:

0 10001001  00110100101001000101100

这个数值不是特殊的情形,可以按照公式(-1)S×2E-127×(1+F)转换。S位的值为(-1)0=1,E位的值为2137-127=1024。F位的值为1+2-3+2-4+2-6+2-9+2-11+2-14+2-18+2-20+2-21= 1.205632686614990234375。最终结果为1×1024×1.205632686614990234375=1234.56787109375。

其中F位比较长,使用二进制方式转换比较麻烦,也可以先转换成十六进制再计算,转换为十六进制如下:

0011 0100  1010  0100 0101  1000

0x3  0x4   0xA   0x4  0x5   0x8

F位为23bits,需要在最后补一个0凑成24bits,共6个十六进制数。F位的值为1+3×16-1+4×16-2+10×16-3+4×16-4+5×16-5+8×16-6=1.205632686614990234375,与上面使用二进制方法得到的结果相同。

2、浮点数换算成二进制数:

下面我们将-987.654e30换算成二进制数。我们先不看符号位,将987.654e30归一化为整数部分为1的形式,也就是写作987.654e30=2E-127×(1+F)的标准形式,其中E=log(987.654e30)/log2+127=109.6+127,取E位的整数值为109+127=236,再求F=987.654e30/2236-127-1=0.52172193,这个小数位数保留8位就够了,已经超出了7位的精度。然后我们求小数部分的二进制数,这个转换就没啥好说的了,依次减去2的幂,从2-1一直到2-23,够减的位置1,不够减的位置0,例如,2-1为0.5,0.52172193-0.5=0.02172193,F位的bit22置1,2-2为0.25,0.02172193不够减,F位的bit21置0,2-3为0.125,0.02172193不够减,F位的bit20置0,2-4为0.0625,0.02172193不够减,F位的bit19置0……,一直算到F位的bit0,这样就得到F位的数值。

如果觉得使用二进制方式转换太麻烦的话也可以使用十六进制进行转换。16-1为0.0625,0.52172193/0.0625=8.3,说明够减8个,记做0x8,0.52172193-0.0625×8=0.02172193,16-2为0.00390625,0.02172193/0.00390625=5.6,说明够减5个,加上刚才的0x8记做0x85,以此类推:

16的-N次幂

被减数

十六进制数

减后的数

1

0.0625

0.52172193

0x8

0.02172193

2

0.00390625

0.02172193

0x85

0.00219068

3

0.000244140625

0.00219068

0x858

0.000237555

4

0.0000152587890625

0.000237555

0x858F

0.0000086731640625

5

0.00000095367431640625

0.0000086731640625

0x858F9

0.00000009009521484375

6

0.000000059604644775390625

0.00000009009521484375

0x858F91

一直凑够23bits,也就是6个十六进制,得到0x858F91,换算成二进制如下所示:

1000 0101  1000  1111 1001  0001

由于只有23bits有效,因此需要去掉最后一个bit,二进制本着0舍1入的原则,变成

1000 0101  1000  1111 1001  001

最后需要再补上前面的S位和E位。由于是负数,S位为1。E位为236,二进制形式为1110 1100,将S、E、F位组合在一起就形成了:

1 1110 1100  1000  0101 1000  1111  1001 001

从左边最高位开始,4个一组合并成十六进制:

1111 0110  0100  0010 1100  0111  1100 1001

换算成十六进制为:

0xF  0x6   0x4   0x2  0xC   0x7  0xC  0x9

所以-987.654e30换算成二进制数为0xF642C7C9。

浮点数的精度

在前面的讲解中可以看到1.xx这个数量级的最小数是2-23,对应的十进制数值为1.00000011920928955078125,可以精确表示到小数点后23位,但有些C语言书上却说float型的有效位只有6~7位,这是为什么?

这是因为二进制小数与十进制小数没有完全一一对应的关系,二进制小数对于十进制小数来说相当于是离散的而不是连续的,我们来看看下面这些数字:

二进制小数        十进制小数

2-23       1.00000011920928955078125

2-22       1.0000002384185791015625

2-21       1.000000476837158203125

2-20       1.00000095367431640625

2-19       1.0000019073486328125

2-18       1.000003814697265625

不看S位和E位,只看F位,上表列出了1.xx这个数量级的6个最小幂的二进制小数,对应的十进制在上表的右边,可以看到使用二进制所能表示的最小小数是1.00000011920928955078125,接下来是1.0000002384185791015625,这两个数之间是有间隔的,如果想用二进制小数来表示8位有效数(只算小数部分,小数点前面的1是隐藏的默认值)1.00000002、1.00000003、1.00000004...这些数是无法办到的,而7位有效数1.0000001可以用2-23来表示,1.0000002可以用2-22来表示,1.0000003可以用2-23+2-22来表示。从这个角度来看,float型所能精确表示的位数只有7位,7位之后的数虽然也是精确表示的,但却无法表示任意一个想表示的数值。

但还是有一些例外的,比如说7位有效数1.0000006这个数就无法使用F位表示,二进制小数对于十进制小数来说相当于是离散的,刚好凑不出1.0000006这个数,从这点来看float型所能精确表示的位数只有6位。至于5位有效值的任何数都是可以使用F位相加组合出来的,即便是乘以E位的指数后也是可以准确表示出来的。

因此float型的有效位数是6~7位,但这个说法应该不是非常准确,准确来说应该是6位,C语言的头文件中规定也是6位。

对于一个很大的数,比如说1234567890,它是F位乘上E位的系数被放大了的,但它的有效位仍然是F位所能表示的6位有效数字。1234567890对应的二进制数是0x4E932C06,其中F位的数值为1.1497809886932373046875,E位的数值为230=1073741824,1073741824×1.1497809886932373046875=1234567936,对比1234567890,也只有高7位是有效位,后3位是无效的。int型定点数可以准确的表示1234567890,而float浮点数则只能近似的表示1234567890,精度问题决定了float型根本无法取代int型。

浮点数的比较

float型的有效位数是6位,那么我们在用float型运算时就要注意了,来看下面这段程序:

#include <stdio.h>int main(void)
{float a = 9.87654321;float b = 9.87654322;if(a > b){printf("a > b\n");}else if(a == b){printf("a == b\n");}else{printf("a < b\n");}return 0;
}

按照我们平时的经验来说这段程序应该走a < b的分支,但程序运行的结果却走了a == b的分支,原因就是float型的精度问题,float型无法区分出小数点后的第8位数,在内存中,a和b的二进制数都是0x411E0652,因此就走了a == b的分支。

某些编译器在编译时会发现a和b的值超出了浮点数的精度,会产生一个告警,提示数据超过精度,但有些编译器则不会做任何提示。最可怕的是有一类编译器,调试窗口里显示的长度超出float型的精度,比如说a的值显示为9.87654321,b的值显示为9.87654322,但在运行时,硬件可不管这套,硬件认为这2个数都是0x411E0652,因此实际运行结果是a == b的分支。

由于精度这个问题的限制,我们在浮点数比较时就需要加一个可接受的精度条件来做判决,比如说上面的这个问题,如果我们认为精度在0.00001就足够了,那么a - b之差的绝对值只要小于0.00001,我们就认为a和b的值是相等的,大于0.00001则认为不等,还要考虑到a - b正负等情况,因此可以将上面的程序改写为:

#include <stdio.h>int main(void)
{float a = 9.87654321;float b = 9.87654322;if(a - b < -0.00001){printf("a < b\n");}else if(a - b > 0.00001){printf("a > b\n");}else{printf("a == b\n");}return 0;
}

例子中a和b之差的绝对值小于0.00001,因此认为a和b是相等的,运行程序,也正确的打印了a == b。

为什么我们要做一个精度的限定?这是因为我们在应用中的精度往往要低于硬件的6位精度。

浮点数的加减

二进制小数与十进制小数之间不存在一一对应的关系,因此某些十进制很整的加减法小数运算由二进制小数来实现就表现出了不整的情况,来看下面的例子:

#include <stdio.h>int main(void)
{float a = 10.2;float b = 9;float c;c= a - b;printf("%f\n", c);return 0;
}

如果用十进制计算的话变量c应该为1.2,在Visual C++ 2010环境下实验输出为1.200000,但实际上c变量的值是1.1999998,只不过是在输出时被四舍五入为1.200000罢了。在内存中c变量的二进制数是0x3F999998,它对应的浮点数是0.19999980926513671875。如果我们将printf函数%f的格式改为%.7f格式,就会看到c变量输出的值是1.1999998。

两个数量级相差很大的数做加减运算时,数值小的浮点数会受精度限制而被忽略,看下面的例子:

#include <stdio.h>int main(void)
{float a = 987654321;float b = 987.654322;float c;c= a + b;printf("%f\n", c);return 0;
}

Visual C++ 2013上的计算结果为987655296.000000,而实际的真实值为987655308.654322,二进制值为0x4E6B79B2对应987655296,就是987655296.000000。可以看出有效值是6位,如果按四舍五入的话可以精确到8位,其中变量b贡献的有效数值只有2位。

987654321

+    987.654322

--------------------------------

987655308.654322    真实值

987655296.000000    计算值

987655296           二进制值,0x4E6B79B2

对于这种数量级相差很大的计算,计算结果会保证高位数有效,数量级小的数相对计算结果显的太小了,不能按自身6位的精度保持,而是需要按照计算结果的6位精度保持。

C语言中有关浮点数的定义

C语言对浮点数做了一些规定,下面是摘自VisualC++ 2013头文件float.h中有关float型的定义,如下:

#define FLT_DIG         6                       /* # of decimal digitsof precision */
#define FLT_EPSILON     1.192092896e-07F        /* smallest such that 1.0+FLT_EPSILON!= 1.0 */
#define FLT_GUARD       0
#define FLT_MANT_DIG    24                      /* # of bits in mantissa*/
#define FLT_MAX         3.402823466e+38F        /* max value */
#define FLT_MAX_10_EXP  38                      /* max decimal exponent*/
#define FLT_MAX_EXP     128                     /* max binary exponent */
#define FLT_MIN         1.175494351e-38F        /* min positive value */
#define FLT_MIN_10_EXP  (-37)                   /* min decimal exponent */
#define FLT_MIN_EXP     (-125)                  /* min binary exponent */

其中FLT_DIG定义了float型的十进制精度,是6位,与我们上面的讨论是一致的。

FLT_EPSILON定义了float型在1.xx数量级下的最小精度,1.xx数量级下判断浮点数是否为0可以使用这个精度。

FLT_MANT_DIG定义了float型F位的长度。

FLT_MAX定义了float型可表示的最大数值。

FLT_MAX_10_EXP定义了float型十进制的最大幂。

FLT_MAX_EXP定义了float型二进制的最大幂。

FLT_MIN定义了float型所能表示的最小正数。

FLT_MIN_10_EXP定义了float型十进制的最小幂。

FLT_MIN_EXP定义了float型二进制的最小幂。

float.h文件里对其它的浮点数也做了规定,有兴趣的读者可以打开float.h头文件继续研究。

转载于:https://www.cnblogs.com/new0801/p/6177121.html

C语言的本质(4)——浮点数的本质与运算相关推荐

  1. 【C 语言】变量本质 ( 变量概念 | 变量本质 - 内存空间别名 | 变量存储位置 - 代码区 | 变量三要素 )

    文章目录 一.变量概念 二.变量本质 1.变量本质 - 内存别名 2.变量存储位置 - 代码区 3.变量三要素 一.变量概念 变量概念 : 变量 是 既能读 , 又能写 的 内存对象 ; 与 变量 相 ...

  2. 【C语言进阶深度学习记录】一 数据类型的本质与变量的本质

    今天学习C语言中的数据类型的本质与变量的本质 文章目录 1 什么是数据类型 2 变量的本质 3 数据类型与变量的关系 4 自定义数据类型与创建变量 5 总结 1 什么是数据类型 数据类型可以理解为固定 ...

  3. C语言中动态内存分配的本质是什么?

    摘要:C语言中比较重要的就是指针,它可以用来链表操作,谈到链表,很多时候为此分配内存采用动态分配而不是静态分配. 本文分享自华为云社区<[云驻共创]C语言中动态内存分配的本质>,作者: G ...

  4. 线性代数的本质与微积分的本质视频分享

    b站上3Blue1Brown Up主的数学视频,感觉真心好,结合着生动的动画来讲解数学.受教颇多.今天分享一下3Blue1Brown线性代数本质与微积分的本质视频.  线性代数的本质 链接:线性代数的 ...

  5. 开心的本质 生理 快乐的本质 幸福的本质 和阈值

    父文章人人都是人生顿悟者,不惑知天命- 人和需求篇 人的意义_个人渣记录仅为自己搜索用的博客-CSDN博客 父文章 人人都是心理学家,mbti等_个人渣记录仅为自己搜索用的博客-CSDN博客 开心的本 ...

  6. 【C 语言】指针 与 数组 ( 指针 | 数组 | 指针运算 | 数组访问方式 | 字符串 | 指针数组 | 数组指针 | 多维数组 | 多维指针 | 数组参数 | 函数指针 | 复杂指针解读)

    相关文章链接 : 1.[嵌入式开发]C语言 指针数组 多维数组 2.[嵌入式开发]C语言 命令行参数 函数指针 gdb调试 3.[嵌入式开发]C语言 结构体相关 的 函数 指针 数组 4.[嵌入式开发 ...

  7. [GO语言基础] 四.算术运算、逻辑运算、赋值运算、位运算及编程练习

    作为网络安全初学者,会遇到采用Go语言开发的恶意样本.因此从今天开始从零讲解Golang编程语言,一方面是督促自己不断前行且学习新知识:另一方面是分享与读者,希望大家一起进步.前文介绍了Golang的 ...

  8. 2.3.2 浮点数的加减运算

    加油哦棒棒哒(●• ̀ω•́ )✧取快递 要如何实现浮点数的运算呢?所以这个小节我们要探讨的是浮点数如何实现加减运算,那除了加减运算的实现之外,我们还会探讨浮点数,还有定点数之间的一个强制类型转换的问 ...

  9. 数据结构源码笔记(C语言):哈希表的相关运算算法

    //实现哈希表的相关运算算法 #include<stdio.h> #include<malloc.h> #include<string.h>#define MaxS ...

最新文章

  1. 安装EBS前期检查工具 - RDA - Health Check / Validation Engine Guide
  2. IOS开发基础知识--碎片39
  3. 实现Operations Manager 2012 R2单一部署
  4. spark集群详细搭建过程及遇到的问题解决(三)
  5. Hibernate:组合模式解决树的映射
  6. Android studio之导入project出现SDK location not found. Define location with sdk.dir in the local.proper
  7. All CUDA devices are used for display and cannot be used while debugging.
  8. 106. 从中序与后序遍历序列构造二叉树
  9. 利用C语言实现计算机图像处理的方法
  10. 阿里ai布局开始_如何开始使用AI
  11. 限制ul显示高度_led显示屏钢结构吊装方案原则及适用条件
  12. Node如何自动重启进程
  13. jdk配置环境变量的方法
  14. LayaAir WebSocket 通信
  15. turtle画动态时钟
  16. 教师资格证信息技术主观题
  17. web 在线word编辑器
  18. origin绘制双Y轴柱状图
  19. 怀孕计算机在线,【孕期天数计算器在线计算_孕期天数计算器在线计算专题】- 天鹅到家...
  20. 冯诺曼伊体系 计算机五大逻辑,科学网—再谈冯·诺伊曼结构 - 姜咏江的博文

热门文章

  1. Win32SDK中(串行)通信资源概要(不断更新)
  2. DynamicMethodHelper R2 2012-08-14 DynamicMethod ILGenerator.Emit
  3. 我记录网站综合系统 -- 技术原理解析[3:我记录框架处理流程]
  4. Oracle创建触发器的普通应用
  5. libboost_filesystem.so: undefined reference to
  6. 【Python-ML】SKlearn库层次聚类凝聚AgglomerativeClustering模型
  7. 【Python学习系列二十四】scikit-learn库逻辑回归实现唯品会用户购买行为预测
  8. Ubuntu下eclipse indigo版在线安装struts插件
  9. Leetcode 107. 二叉树的层次遍历 II 解题思路及C++实现
  10. Pandas简明教程:五、Pandas简单统计操作及通用方式