☀️光天化日学C语言☀️(25)- 浮点数的精度问题 | 浮点数判等千万不要写成 a == b

Posted 英雄哪里出来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了☀️光天化日学C语言☀️(25)- 浮点数的精度问题 | 浮点数判等千万不要写成 a == b相关的知识,希望对你有一定的参考价值。

🙉饭不食,水不饮,题必须刷🙉

还不会C语言,和我一起打卡!
🌞《光天化日学C语言》🌞

LeetCode 太难?上简单题!
🧡《C语言入门100例》🧡

LeetCode 太简单?大神盘他!
🌌《夜深人静写算法》🌌

一、前言

  本文作者是从 2007 年开始学 C语言 的,不久又接触了C++,基本就是 C/C++ 技术栈写了 14 年的样子,不算精通,但也算差强人意。著有《夜深人静写算法》系列,且承诺会持续更新,直到所有算法都学完。主要专攻 高中 OI 、大学 ACM、 职场 LeetCode 的全领域算法。由于文章中采用 C/C++ 的语法,于是就有不少读者朋友反馈语言层面就被劝退了,更何况是算法。
  于是,2021 年 06 月 12 日,《光天化日学C语言》 应运而生。这个系列文章主要服务于高中生、大学生以及职场上想入坑C语言的志同道合之人,希望能给祖国引入更多编程方面的人才,并且让自己的青春不留遗憾!
  这一章的主要内容将的是浮点数的精度问题。

二、人物简介

  • 第一位登场的就是今后会一直教我们C语言的老师 —— 光天。
  • 第二位登场的则是今后会和大家一起学习C语言的没什么资质的小白程序猿 —— 化日。

三、精度问题的原因

  • 对于十进制的数转换成二进制时,整数部分和小数部分转换方式是不同的。

1、整数转二进制

  • 对于整数而言,采用的是 “展除法”,即不断的除以 2,取余数。
  • 举例, ( 11 ) 10 (11)_{10} (11)10 通过不断除2,取余数,得到的余数序列为 1   1   0   1 1 \\ 1 \\ 0 \\ 1 1 1 0 1,然后逆序一下, ( 1011 ) 2 (1011)_2 (1011)2 就是它的二进制表示了。
  • 所以对于一个有限位数的整数,一定能够转换成有限位数的二进制。

2、小数转二进制

  • 而对于小数而言,采用的是 “乘二取整法”,即 不断乘以 2,取整数。一个有限位数的小数不一定能够转换成有限位数的二进制。只有末尾是 5 的小数才有可能转换成有限位数的二进制。
  • 在之前的章节中,我们知道floatdouble的尾数部分是有限的,可定无法容纳无限的二进制数,即使能够转换成有限的位数,也可能会超出给定的尾数部分的长度,这时候就必须进行舍弃。这时候,由于和原数并不是完全相等,就出现了精度问题。

3、四舍五入

  • 对与float类型,是一个四字节的浮点数,也就是32个比特位,具体内存存储方式如下图所示:

  • 而对于double类型,是一个八字节的浮点数,也就是64个比特位,具体内存存储方式如下图所示:

  • 所以对于float的二进制表示,尾数23位,加上一位隐藏的1,总共24位,最后一位可能是精确数字,也可能是近似数字;而其余的 23 位都是精确数字。从二进制的角度看,这种浮点格式的小数,最多有 24 位有效数字,但是能保证的是 23 位;也就是说,整体的精度为 23 ~ 24 位。如果转换成十进制, 2 24 = 16777216 2^{24} = 16777216 224=16777216,一共 8 位;也就是说,最多有 8 位有效数字(十进制),但是能保证的是 7 位,从而得出整体精度为 7 ~ 8 位。对于 double,同理可得,二进制形式的精度为 52 ~ 53 位,十进制形式的精度为 15 ~ 16 位。
浮点数类型尾数个数(二进制)十进制位数
float23 ~ 247 ~ 8
double52 ~ 5315 ~ 16

四、IEEE 754 标准

  • 浮点数除了 光天化日学C语言(24)- 浮点数的存储 讲到的存储方式以外,还遵循 IEEE 754 标准。
  • IEEE 754 标准规定,当指数 e x p o n e n t exponent exponent 的所有位都为 1 时,不再作为 “正常” 的浮点数对待,而是作为特殊值处理。

1、负无穷大

  • 如果此时尾数 f r a c t i o n fraction fraction 的二进制位都为 0,且符号 sign 为 1,则表示负无穷大;
#include <stdio.h>

int main() {
    int ninf = 0b11111111100000000000000000000000;
    printf("%f\\n", *(float *)&ninf );
    return 0;
}
  • 运行结果为:
-inf

2、正无穷大

  • 如果此时尾数 f r a c t i o n fraction fraction 的二进制位都为 0,且符号 sign 为 0,则表示正无穷大。
#include <stdio.h>

int main() {
    int pinf = 0b01111111100000000000000000000000;
    printf("%f\\n", *(float *)&pinf );
    return 0;
}
  • 运行结果为:
inf

3、Not a Number

  • 如果此时尾数 f r a c t i o n fraction fraction 的二进制位不全为 0,则表示 NaN (Not a Number),也即这是一个无效的数字,或者该数字未经初始化。
#include <stdio.h>

int main() {
    int nan  = 0b11111111100000000000000000001010;
    printf("%f\\n", *(float *)&nan );
    return 0;
}
  • 运行结果如下,符合我们的预期:
nan

4、浮点数的规格化

  • 当指数 e x p o n e n t exponent exponent 的所有二进制位都为 0 时,情况也比较特殊。对于 “正常” 的浮点数,尾数 f r a c t i o n fraction fraction 隐含的整数部分为 1,并且在读取浮点数时,内存中的指数 e x p exp exp 要减去中间值 2 n − 1 − 1 2^{n-1}-1 2n11 才能还原真实的指数 e x p o n e n t exponent exponent
  • 然而,当指数 e x p exp exp 的所有二进制位都为 0 时,尾数隐含的整数部分变成了 0,并且用 1 减去中间值 2 n − 1 − 1 2^{n-1}-1 2n11 才能还原真实的指数 e x p o n e n t exponent exponent
  • 为什么会有非规格化浮点数的出现呢?
  • 来看这样一个例子!
  • 在规格化浮点数中,浮点数的尾数不应当包含前导 0。如果全部用十进制表示,对于类似0.012的浮点数,规格化的表示应为1.2e-2。但对于某些过小的数,如1.2e-130,允许的指数位数不能满足指数的需要,可能就会在尾数前添加前导0,如将其表示为0.00012e-126

总结如下:
  1)当指数 e x p exp exp 的所有二进制位都是 0 时,我们将这样的浮点数称为“非规格化浮点数”;
  2)当指数 e x p exp exp 的所有二进制位既不全为 0 也不全为 1 时,我们称之为“规格化浮点数”;
  3)当指数 exp 的所有二进制位都是 1 时,作为特殊值对待。
换言之,究竟是规格化浮点数,还是非规格化浮点数,还是特殊值,完全看指数 e x p exp exp

五、浮点数判定

1、精度定义

  • 在 C++ 中, 1 e − 6 1e-6 1e6 代表 1 0 − 6 10^{-6} 106,即 0.000001 0.000001 0.000001,是一个比较合适的精度值;
#define eps 1e-6

2、相等判定

  • 介于浮点数的表示方式,不能用 ‘==’ 进行相等判定,必须将两数相减,取绝对值以后,根据结果是否小于某个精度来判定两者是否相等;
bool EQ(double a, double b) {   // EQual
	return fabs(a - b) < eps;
}

3、不相等判定

  • ‘不相等’ 就是 ‘相等’ 的 ‘非’(取反);
bool NEQ(double a, double b) {  // NotEQual
	return !EQ(a, b);
}

4、大于等于判定

  • ‘大于等于’ 就是 ‘大于 或 等于’ 的意思,需要拆分成如下形式:
bool GET(double a, double b) {    // GreaterEqualThan
	return a > b || EQ(a, b);
}

5、小于等于判定

  • ‘小于等于’ 就是 ‘小于 或 等于’ 的意思,需要拆分成如下形式:
bool SET(double a, double b) {   // SmallerEqualThan
	return a < b || EQ(a, b);
}

6、小于判定

  • ‘小于’ 就是 ‘大于等于’ 的 ‘非’,需要拆分成如下形式:
  • 注意:千万不能直接用 a < b a < b a<b
bool ST(double a, double b) {   // SmallerThan
	return a < b && NEQ(a, b);
}

#加粗样式# 7、大于判定

  • ‘大于’ 就是 ‘小于等于’ 的 ‘非’,需要拆分成如下形式:
  • 注意:千万不能直接用 a > b a > b a>b
bool GT(double a, double b) {   // GreaterThan
	return a > b && NEQ(a, b);
}

通过这一章,我们学会了:
  浮点数的精度及其判定;

  • 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!

课后习题


📢博客主页:https://blog.csdn.net/WhereIsHeroFrom
📢欢迎各位 👍点赞 ⭐收藏 📝评论,如有错误请留言指正,非常感谢!
📢本文由 英雄哪里出来 原创,转载请注明出处,首发于 🙉 CSDN 🙉
作者的专栏:
  👉C语言基础专栏《光天化日学C语言》
  👉C语言基础配套试题详解《C语言入门100例》
  👉算法进阶专栏《夜深人静写算法》

以上是关于☀️光天化日学C语言☀️(25)- 浮点数的精度问题 | 浮点数判等千万不要写成 a == b的主要内容,如果未能解决你的问题,请参考以下文章

☀️光天化日学C语言☀️(34)- 函数进阶 | 面向过程编程

☀️光天化日学C语言☀️(33)- 函数入门 | 开启下一个篇章!

☀️光天化日学C语言☀️(35)- 局部变量和全局变量

☀️光天化日学C语言☀️(32)- continue 关键字 | 下一个!

☀️光天化日学C语言☀️(29)- while 语句 | 死循环啦!

☀️光天化日学C语言☀️(31)- break 关键字 | 当断则断!