计算机进行小数运算时出错的原因

0、引言

本文为作者读程序是怎样跑起来的一书第三章的读书笔记。

1、将0.1累加100次也得不到10

看下面一段程序:

#include <stdio.h>

void main()
{
    float sum;
    int i;
    sum = 0;

    // 0.1相加100次
    for (i = 1; i <= 100; i++) {
        sum += 0.1;
    }
    printf("%f\n", sum);
}
复制代码

上面的程序功能是把0.1累加100次后,然后将累加后的值打印出来。我们知道,这个预期值应该是10。但是运行结果并不是10。

// 运行结果:
10.000002
复制代码

那这段程序运行出错的原因是什么呢?我们知道,计算机内部存储的信息都是以二进制的形式表示出来的。所以小数在计算机内部也是由二进制表示的。但是”有一些十进制的小数无法转换成二进制数”。十进制数0.1就是这种情况,转换成二进制后变成0.00011001100…(1100训话)(就像无法用十进制数表示1/3一样。1/3就是0.3333…)。

2、用二进制数表示小数

用二进制数表示小数只需将各数位数值和位权相乘,然后再将相乘的结果相加即可。
二进制数小数点前面部分的位权,第1位是2的0次幂,第2位是2的1次幂……以此类推。小数点后面部分的位权,第1位是2的-1次幂,第2位是2的-2次幂,以此类推。
比如二进制数1011.0011表示的十进制数就是:1 * 2^3 + 0 * 2^2 + 1 * 2^1 + 1 * 2^0 + 0 * 2^-1 + 0 * 2^-2 + 1 * 2^-3 + 1 * 2^-4 = 11.1875

3、什么是浮点数

像1011.0011这样带小数点的二进制表现形式,完全是我们纸面上的二进制表现形式,在计算机内部是无法使用的。
计算机内部是用浮点数来表示的。比如C语言中有双精度浮点数和单精度浮点数。双精度浮点数类型用64位,单精度浮点数类型用32位来表示全体小数。
浮点数是指用符号、尾数、基数和指数这四部分来表示的小数。因为计算机内部使用的是二进制数,所以基数是2.因此,实际的数据中往往不考虑基数,只用符号、尾数、指数这三部分即可表示浮点数。

// 浮点数的表现形式
±m*n^e
其中±是符号,m是尾数,n是基数,e是指数
复制代码

浮点数的表现方式有很多种,最为普遍的是IEEE标准:

双精度浮点数.png

单精度浮点数.png

符号部分是指使用一个数据位来表示数值的符号。该数据位是1表示负,0表示正。
尾数部分用的是”将小数点前面的值固定为1的正则表达式”。
指数部分用的则是”EXCESS系统表现”。

3.1、正则表达式

尾数部分用的是正则表达式。具体是说,在二进制数中,我们使用的是”将小数点前面的值固定为1的正则表达式“。也就是说,将二进制数表示的小数左移或者右移(逻辑移位,因为符号位是独立的)数次后,整数部分的第1位是1,第2位之后都是0。而且,第1位的1在实际的数据中不保存。由于第1位必须是1,因此,省略该部分后就节省了一个数据位,从而也就可以表示更多的数据范围。
比如,二进制数1011.0011的单精度浮点数尾数部分的正则表达式就是如下的表示方法:

1011.0011 // 原始数据
    |
0001.0110011 // 右移使整数部分的第1位变成1
    |
0001.0110011000000000000000 // 确保小数点以后的长度为23位
    |
0110011000000000000000 // 仅宝轮小数点后面的部分,完成正则表达式
复制代码

3.2、EXCESS系统

EXCESS系统是指,通过将指数部分表示范围的中间值设为0,使得负数不需要用符号位来表示。也就是说,当指数部分是8位单精度浮点数时,最大值为11111111=255的1/2,即01111111 =127(小数部分舍弃)表示的是0。指数部分是11位双精度浮点数时,1111111111=2047的1/2即0111111111=1023(小数部分舍弃)表示的是0。
上面说的可能不太好理解。举个例子。假设有这样一个游戏。用113(AK)的扑克牌来表示负数。这时,我们可以把中间的7这张牌当成0,。如果扑克牌7是0,则扑克牌10就表示+3,扑克牌3就表示-4。这个就是EXCESS系统。

4、在实际的程序中进行确认

#include <stdio.h>
#include <string.h>

// 函数功能:用于确认单精度浮点数表示方法
void main()
{
    float data;
    unsigned long buff;
    int i;
    char s[34];

    // 将0.75以单精度浮点数的形式存储在变量data中
    data = (float)0.75;

    // 把数据复制到4字节长度的整数变量buff中以逐个提取出每一位
    memcpy(&buff, &data, 4);

    // 逐一取出每一位
    for (i = 33; i >=0; i--) {
        if (i == 1 || i == 10) {
            // 加入破折号来区分符号部分、指数部分和尾数部分
            s[i] = '-';
        } else {
            // 为各个字节赋值'0'或者'1'
            if (buff % 2 == 1) {
                s[i] = '1';
            } else {
                s[i] = '0';
            }
            buff /= 2;
        }
    }
    s[34] = '\0';

    printf("%s\n", s);
}
复制代码

该程序执行后,运行结果是:

0-01111110-10000000000000000000000
复制代码

也就是说十进制数0.75用单精度浮点数来表示就变成了0-01111110-10000000000000000000000
这里,符号位为0.指数部分为01111110,是十进制的126,用EXCESS系统表现就是-1(126-127)。尾数部分是10000000000000000000000,实际上表示的是1.10000000000000000000000这个二进制数,换成十进制数就是1.5。
因此,0-01111110-10000000000000000000000这个单精度浮点数,表示的就是”1.5 * 2的-1次幂”,即+1.5*0.5 = +0.75。正好吻合,结果正确。

将程序中的0.75换成0.1.就得到了用单精度浮点数表示十进制数0.1。结果是0-01111011-10011001100110011001101。但是如果反过来计算这个数值的十进制,你会发现居然不是0.1。这就是刚开始说0.1累加100次得不到10的原因。

data = (float)0.75;
复制代码

5、如何避免计算机计算出错

  1. 回避策略,即无视这些错误。
  2. 把小数转换成整数来计算。(因为整数计算一定不会出错)

例如,将0.1相加100次的计算,可以转换成0.1扩大10倍后再将1相加100次的计算,最后把结果除以10就可以了。

#include <stdio.h>

// 函数功能:用于确认单精度浮点数表示方法
void main()
{
    int sum;
    int i;

    sum = 0;
    for (i = 1; i <= 100; i++) {
        sum += 1;
    }

    // 总和结果除以10
    sum /= 10;

    printf("%d\n", sum);
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享