浮点数的近似保存与计算

负数的补码存储

首先我们回忆一下负数的补码表示。我们都知道,有符号数的负数使用补码的方式进行存储:

补码表示

之所以这样,就是 方便统一运算 ,如果负数不是使用补码的方式表示,则在做基本对加减法运算的时候, 还需要多一步操作来判断是否为负数,如果为负数,还得把加法反转成减法,或者把减法反转成加法 ,这就非常不好了,毕竟加减法运算在计算机里是很常使用的,所以为了性能考虑,应该要尽量简化这个运算过程。

image-20230713150803755

十进制浮点数与二进制的转换

有限循环的二进制

这种最简单的情况,直接遵照我们的默认转换方法:

  • 整数部分采用:除 2 取余法
  • 小数部分采用:乘 2 取整法

其计算过程如下图所示:

image-20230713203912197

反之二进制转换为十进制则较为简单,按位乘以2的对应幂次即可:

image-20230713204023335

无限循环的二进制

上面所说的是可以用有限位二进制表示的十进制数,但是还有的数字是无法用有限位二进制来表达的,它们转换的过程中变成了无限循环的二进制。

例如按照之前的算法,对于 0.1 ,转换为二进制,过程如下:

image-20230713212239487

可以发现,0.1 的二进制表示是无限循环的。

由于计算机的资源是有限的,所以是没办法用二进制精确的表示 0.1,只能用「近似值」来表示,就是在有限的精度情况下,最大化接近 0.1 的二进制数,于是就会造成精度缺失的情况 。在后面会介绍0.1这种无限循环二进制数是如何在计算机中保存的。

计算机对浮点数的保存

现在绝大多数计算机使用的浮点数,一般采用的是 IEEE 制定的国际标准,这种标准形式如下图:

image-20230713205333734

这三个重要部分的意义如下:

  • 符号位:表示数字是正数还是负数,为 0 表示正数,为 1 表示负数;
  • 指数位:指定了小数点在数据中的位置,指数可以是负数,也可以是正数,指数位的长度越长则数值的表达范围就越大
  • 尾数位:小数点右侧的数字,也就是小数部分,比如二进制 1.0011 x 2^(-2),尾数部分就是 0011,而且尾数的长度决定了这个数的精度,因此如果要表示精度更高的小数,则就要提高尾数位的长度;

32 位来表示的浮点数,则称为单精度浮点数,也就是我们编程语言中的 float 变量,而用 64 位来表示的浮点数,称为双精度浮点数,也就是 double 变量,它们的结构如下:

image-20230713205421249

2进制小数转换为2进制浮点数保存的步骤如下:

image-20230713205626351

可以注意到转换过程中:

  1. 首先要移动小数点到第一个有效数字后面,然后小数点右侧的数字就是浮点数里的尾数位存储的值。
  2. 指数位在存储时以+127的方式进行;这样就可以把指数转换成无符号数,可以表达的指数取值范围在 -126 ~ +127
  3. 移动后的小数点左侧的有效位(即 1)消失了,因为 IEEE 标准规定默认左侧最高位就是1

因此转换公式为:

image-20230713210301657

我们举个例子,例如将 -5.125 转换成float类型进行保存,则按照IEEE 754标准的规则进行以下步骤:

  1. 符号位:由于-5.125是负数,符号位为1。
  2. 绝对值的二进制表示:将5.125的绝对值转换为二进制形式,得到101.001。
  3. 规范化:将二进制表示规范化,即将小数点左移,直到只有一位非零数字位,得到1.01001。
  4. 指数位:计算小数点左移的位数,这里是2。
  5. 偏移量:对于32位浮点数,偏移量为127。将实际指数值2加上偏移量,得到指数位的值为129。
  6. 最终二进制表示:将符号位、指数位和尾数位按顺序连接起来,得到最终的32位二进制表示: 1 10000001 01001000000000000000000

我们编写代码进行测试,这里使用联合体(union)来转换浮点数和二进制数据:

#include <iostream>
#include <iomanip>

union FloatBinary {
    
    
    float f;
    unsigned int i;
};

int main() {
    
    
    float num = -5.125;
    FloatBinary fb;
    fb.f = num;

    // 将二进制表示以字符串形式打印
    std::cout << num << "\t-->\t";
    for (int i = 31; i >= 0; --i) {
    
    
        std::cout << ((fb.i >> i) & 1);
        if (i == 31 || i == 23) {
    
    
            std::cout << ' ';
        }
    }
    std::cout << std::endl;

    return 0;
}

测试结果如下:

image-20230713212929680

和我们上面按照默认步骤计算的结果是一致的。

无限循环二进制数的保存

上面提到 0.1 这种无限循环的二进制数,对于这种数字,计算机是如何保存的呢?

使用 binaryconvert 这个工具,将十进制 0.1 小数转换成 float 浮点数,观察如下:

image-20230713212505777

可以看到,8 位指数部分是 01111011,23 位的尾数部分是 10011001100110011001101,可以看到 尾数部分是 0011 是一直循环的 ,只不过尾数是有长度限制的,所以只会显示一部分,所以是一个近似值,精度十分有限。

我们用刚刚的程序在代码中观察一下,发现和显示的一致:

image-20230713212916249

再看看 0.2 的保存方式:

image-20230713212607960

可以看到,8 位指数部分是 01111100,稍微和 0.1 的指数不同,23 位的尾数部分是 10011001100110011001101 和 0.1 的尾数部分是相同的,也是一个近似值。

再用代码观察,发现也是一致的:

image-20230713213016206

浮点数的近似

我们再将两个浮点数转换回十进制:

image-20230713213133665

这两个结果相加就是 0.300000004470348358154296875

image-20230713213201605

可以得出,计算机里对这样的浮点数采取近似保存,所以其相加得出的也是一个近似数。我们用代码测试一下:

#include <iostream>
#include <iomanip>

int main() {
    
    
    double num1 = 0.1f;
    double num2 = 0.2f;
    double sum = num1 + num2;

    std::cout << std::setprecision(32);
    std::cout << "num1:\t\t" << num1 << std::endl;
    std::cout << "num1:\t\t" << num2 << std::endl;
    std::cout << "num1+num2:\t" << sum << std::endl;

    return 0;
}

image-20230713214410526

至于这里为什么将三个数据类型都设定为了double,是因为float的时候,相加的结果并不会等于我们预想的值:

image-20230713214503319

之所以这样,是因为当进行相加操作时,0.1和0.2的近似值参与计算,由于浮点数精度有限,可能 会产生进一步的近似和舍入误差 。因此,相加的结果变成了 0.300000011920928955078125 ,与我们期望的精确结果0.3有微小的差异。

参考文献

  1. 2.7 为什么 0.1 + 0.2 不等于 0.3 ? | 小林coding


部分图片来源网络,如有侵权请联系我删除。
如有疑问或错误,欢迎和我私信交流指正。
版权所有,未经授权,请勿转载!
Copyright © 2023.07 by Mr.Idleman. All rights reserved.


猜你喜欢

转载自blog.csdn.net/qq_42059060/article/details/131712371