【解码】浮点数精度问题 | 为什么(int)(32.3 x 100) = 3229?

零 | 序

前几天在找一个代码问题时,苦思不得其解,简直要怀疑人生。查看各种参数,输入输出,都符合条件,最后各种排除法之后,定位到一段简单的代码,简化后大致如下:

#include<stdio.h>

int main()
{
    double a = 32.3;
    int    b = 100;
    int    c = (int)(a*b);
    printf("c = %d",c); //c = 3229
    return 0;
}

原代码中本来预想c应该会等于3230,可是最后的结果却是3229!!!

第一反应就觉得应该是浮点数精度问题,但是怎么条理清晰地向别人解释呢?好像有点难度,于是回家认真翻阅了下书籍,整理了一下思路。

一个简单的解释是:

我们都知道计算机中只有0和1,也没有小数点,因此要表示浮点数时有自己的一套表示方法,这套表示方法在有限位数情况下有时并不能精确的表示某个浮点数,只能尽量逼近它,例如这个例子中,我们定义了一个double型的32.3,我们以为它表示32.3,但是计算机用有限长的0和1只能表示32.2999999......,这样当这个数乘上100时,就变成了3229.99999,当它从double转型成int时,小数点被舍掉了,就变成了3229。

这个解释......好像似懂非懂的样子,那么问题来了:

1. 浮点数在计算机中到底是怎么存储的?为什么有的小数无法精确表示?

2. 浮点数乘法是怎么实现的?

3. double转型成int时,为什么会把小数舍掉?

一 | 浮点数表示

要理解上面的问题,我们先从更简单的2进制小数开始,我们知道在10进制中:123.45 = 1 x 102 + 2 x 101 + 3 x 100 + 4 x 10-1 + 5 x 10-2

类似的2进制小数也可以这样表示:101.11 = 1 x 22 + 0 x 21 + 1 x 20 + 1 x 2-1 + 1 x 2-2 = 5.75,

如果考虑有限长度,我们知道1/3在10进制中没办法准确表示,同样的,二进制中也有不能精确表示的数,如1/5,二进制只能表示那些能被写成a x 2b的数,就像上面的5.75。

所以本文开始的例子中32.3 = 0010 0000.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 ...... = 32.29999999999......

这就解释了为什么32.3在计算机中是32.999999999999......

等等!不是说计算机中没有小数点的吗?

确实如此,因此在实际计算机中,采用的是IEEE浮点表示法(IEEE-754标准),即V=(-1)s x (1.M) x 2E-f表示一个浮点数,其中s是符号,M是尾数,E是阶码,其存储规则如下:

单精度格式(32位):符号位(s)1位;阶码(E)8位,阶码的偏移量(f)为127(7FH);尾数(M)23位,用小数表示,小数点放在尾数域的最前面;
双精度格式(64位):符号位(s)1位;阶码(E)11位,阶码的偏移量(f)为1023(3FFH);尾数(M)52位,用小数表示,小数点放在尾数域的最前面。

举个简单的例子:(1.75)10 = (1.11)2 = 1.11 x 20,所以在单精度格式中s = 0,M = 11,E = 127 = (01111111)2

因此在计算机中,float型的1.75存储为 0 01111111 11000000000000000000000 = (3FE00000)16

而double型的1.75存储为(3FFC000000000000)16,这个就留给您自己去推算一遍了。

下面我们通过一段代码来验证一下上面的原理,证实1.75在计算机中确实是这样存储的。

首先我们定义一个指向类型为unsigned char的对象指针,然后定义一个show_bytes方法,打印出每个以16进制表示的字节,%.2x表示整数必须用至少两个数字的十六进制格式输出。接着定义show_int,show_float,show_double分别调用show_bytes,根据不同的类型和长度,打印出对应的字节表示。

#include<stdio.h>

typedef unsigned char *byte_pointer; //定义一个指向类型为unsigned char的对象指针
//以16进制打印指针指向地址中的字节序列
void show_bytes(byte_pointer start, int len){
    int i;
    for (i = 0; i < len; i++)
        printf("%.2x",start[i]);
    printf("\n");
}
//打印整数型变量
void show_int(int x){
    show_bytes((byte_pointer)&x, sizeof(int));
}
//打印单精度浮点变量
void show_float(float x){
    show_bytes((byte_pointer)&x, sizeof(float));
}
//打印双精度浮点变量
void show_double(double x){
    show_bytes((byte_pointer)&x, sizeof(double));
}

//主程序int main()
{
    double a = 1.75;
    show_float(a);
    show_double(a);return 0;
}

运行结果为:

0000e03f
000000000000fc3f

注意到这里结果似乎跟我们推算的值不太一样,这是因为我的计算机采用小端法存储(这个概念如不清楚请Google之),即把低序的存在低地址,所以00 00 e0 3f从高地址开始读就是3f e0 00 00。那么本文一开始提到的32.3在双精度中是怎么表示的呢?修改程序后运行可得:

cdcc0042
9a99999999194040

二 | 浮点数乘法

搞清楚了浮点数的存储方式,我们来看看浮点数的乘法是怎么实现的。假设有两个浮点数:

x = Mx x 2Ex  y = My x 2Ey

那么x*y =( Mx x 2Ex ) ( My x 2Ey ) = 2Ex+Ey·(Mx * My),

也就是说两个浮点数相乘的结果就是它们的阶码相加,尾数相乘

所以在双精度中32.3 x 100 = (1.0000 0010 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 x 25) x (1.1001 x 26)

            = 1.1001 0011 1011 1111 1111 1111 1111... x 211

            = (0100 0000 1010 1001 0011 1011 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111)2

            = 3299.9999999999......

这里是以二进制小数的方式简单说明了下浮点数的乘法,虽然浮点数的乘法可以转换成定点数的加法和乘法,但我们知道在计算机中的0和1,也并没有真正的“加法”和“乘法”,所有的操作是通过寄存器和逻辑门操作完成的,想要真正“理解”浮点数乘法操作是怎么实现的,不妨研读下“汇编语言”相关内容。

PS. 顺便说一下,浮点数32.3乘整数100,按C语言的规则是100转成浮点数再运算,而不是32.3先转成整数再运算。

三 | 浮点数转型成整数

浮点数转型成整数时,会把小数点舍掉,有人说,这是C语言规定的,没什么好解释的。但是计算机总有自己的一套规则吧,究竟是怎么转换的呢?这方面容我再好好深入学习下《汇编原理》和《深入理解计算机系统》,再来向各位汇报,也欢迎各位大神指导。

另外,如果把文章开头的double a = 32.3变成float a = 32.3,结果c会变成3230,各位读者如果有兴趣可以思考下为什么。

总结一下,浮点有风险,使用需谨慎!

参考文献:

1. 《深入理解计算机系统(第2版)》 机械工业出版社

2. https://en.wikipedia.org/wiki/IEEE_754_revision

3. http://share.onlinesjtu.com/mod/tab/view.php?id=176

原文地址:https://www.cnblogs.com/kplayer/p/9410476.html

时间: 2024-10-12 21:06:23

【解码】浮点数精度问题 | 为什么(int)(32.3 x 100) = 3229?的相关文章

又见浮点数精度问题

今天看到一篇文章:http://younglab.blog.51cto.com/416652/241886,大概是说在使用Javascript进行下面的浮点数计算时出现了问题: obj.style.opacity =  (parseInt(obj.style.opacity *100) + 1)/100; obj.style.opacity是一个浮点数,范围从0~1,初始值为0.这句代码每隔一小段时间执行一次,从而让目标由透明慢慢变为不透明(淡入效果). 问题是,起初obj.style.opac

一个由于浮点数精度导致的错误

今天看到一篇文章:http://younglab.blog.51cto.com/416652/241886,大概是说在使用Javascript进行下面的浮点数计算时出现了问题: obj.style.opacity = (parseInt(obj.style.opacity *100) + 1)/100; obj.style.opacity是一个浮点数,范围从0~1,初始值为0.这句代码每隔一小段时间执行一次,从而让目标由透明慢慢变为不透明(淡入效果).起初,obj.style.opacity能够

Java 浮点数精度丢失

Java 浮点数精度丢失 问题引入 昨天帮室友写一个模拟发红包抢红包的程序时,对金额统一使用的 double 来建模,结果发现在实际运行时程序的结果在数值上总是有细微的误差,程序运行的截图: 输入依次为:红包个数,抢红包的人数,选择固定金额红包还是随机金额红包,每个红包的金额(此例只有一个红包). 注意到程序最后的结果是有问题的,我们只有一个金额为 10 的红包,一个人去抢,所以正确结果应该为这个人抢到了 10 RMB. 为了使问题更加明显,我们测试一个更加简单的例子: public class

浮点数精度问题

浮点数精度问题: public static void main(String[] args) { double d = 11.4; int i = 13; double a = d*i; System.out.println("反例:"+a); BigDecimal num12 = new BigDecimal(d); BigDecimal num22 = new BigDecimal(i); BigDecimal result32 = num12.multiply(num22);

mingw fbx sdk /浮点数精度

接下来要做一个linux下的程序了. 下载linux version     fbx sdk tar zxvf ...gz 按照安装说明 提升权限并没什么用 还是,cannot execute binary file 感觉是版本的问题,也就是说我要用f extension bx sdk这个版本 是dll的 vs跑完用mingw windows + vs2013用的肯定是 febx sdk windows version mingw 下面,据说那只是gcc而不意味着 linux所以...也许还是要

.Net Entity Framework Core 用 HasColumnType 配置浮点数精度

一.前言 前段时间用.Net Entity Framework core搭建框架,需要配置浮点数的精度,发现.Net Entity Framework core 并没有HasPrecision方法.在网上查找资料也比较少,最后通过官方文档说明,尝试使用HasColumnType配置浮点数精度成功. 二.HasColumnType官方文档说明 文档连接: https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.entityframeworkcor

用decimal模块增加python的浮点数精度

浮点数python默认是17位精度,也就是小数点后16位(16位以后的全部四舍五入了),虽然有16位,但是这个精度越往后越不准. 如果有特殊需求,需要更多的精度,可以用decimal模块,通过更改其里面getcontext()函数里面的prec参数,来决定你想要的浮点数精度. from decimal import Decimal from decimal import getcontext work_context = getcontext() work_context.prec = 100

js浮点数精度丢失问题及如何解决js中浮点数计算不精准

js中进行数字计算时候,会出现精度误差的问题.先来看一个实例: console.log(0.1+0.2===0.3);//false console.log(0.1+0.1===0.2);//true 上面第一个的输出会超出我们的常识,正常应该为true,这里为什么会是false呢,直接运行会发现0.1+0.2在js中计算的结果是: console.log(0.1+0.2);//输出0.30000000000000004 这对于浮点数的四则运算(加减乘除),几乎所有的编程语言都会出现上面类似的精

Java之道系列:BigDecimal如何解决浮点数精度问题

如题,今天我们来看下java.math.BigDecimal是如何解决浮点数的精度问题的,在那之前当然得先了解下浮点数精度问题是什么问题了.下面我们先从IEEE 754说起. IEEE 754 IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用.这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number)),一些特殊数值(无穷(Inf)与非数值(NaN)),以及这些数值的"浮点数运算符&qu