浮点数的误差问题

一直很奇怪C#的预定义数据类型中为什么加了一个decimal,有floatdouble不就够了吗?今天来挖一挖。

浮点型

Name

CTS Type

Description

Significant Figures

Range (approximate)

float

System.Single

32-bit single-precision floating point

7

±1.5 × 10−45 to ±3.4 × 1038

double

System.Double

64-bit double-precision floating point

15/16

±5.0 × 10 −324 to ±1.7 × 10308

如果我们在代码中写一个12.3,编译器会自动认为这个数是个double型。所以如果我们想指定12.3float类型,那么你必须在数字后面加上F/f
float f = 12.3F;

decimal类型

作为补充,decimal类型用来表示高精度的浮点数

Name

CTS Type

Description

Significant Figures

Range (approximate)

decimal

System.Decimal

128-bit high precision decimal notation

28

±1.0 × 10−28 to ±7.9 × 1028

从上表可以看出,decimal的有效位数很大,达到了28位,但是表示的数据范围却比floatdouble类型小。decimal类型并不是C#中的基础类型,所以使用的时候会对计算时的性能有影响。

decimalfloatdouble运算结果出现误差的分析

我们知道将一个十进制数值转换为二进制数值,需要通过下面的计算方法:

1. 整数部分:连续用该整数除以2,取余数,然后商再除以2,直到商等于0为止。然后把得到的各个余数按相反的顺序排列。简称"2取余法"

2. 小数部分:十进制小数转换为二进制小数,采用"2取整,顺序排列"法。用2乘以十进制小数,将得到的整数部分取出,再用2乘余下的小数部分,然后再将积的整数部分取出,如此进行,直到积中的小数部分为0或者达到所要求的精度为止。然后把取出的整数部分按顺序排列起来,即先取出的整数部分作为二进制小数的高位,后取出的整数部分作为低位有效位。简称"2取整法"

3. 含有小数的十进制数转换成二进制,整数、小数部分分别进行转换,然后相加。

例如:将十进制数值25.75转换为二进制数值,步骤如下:

25(整数部分)

25/2=12......1

12/2=6.......0

6/2=3......0

3/2=1......1

1/2=0......1

(25) 10=(11001) 2

0.75(小数部分)

0.75*2=1.5......1

0.5*2=1......1

(0.75) 10=(0.11) 2

(25.75) 10=(11001) 2+(0.11)2=(11001.11) 2

按照上述方法,我们将0.650.6转换为二进制代码:

(0.65)10 =(0.101001100110011001100110011001100110011......)2

(0.6) 10 =(0.10011001100110011001100110011001100110011......)2

后面的省略号表示已经算不完了,后面在无限重复 0011这段二进制数值。

文章开始部分,我们用的float类型,下面我们来看看float类型是否能存储上面转换出的二进制代码。

目前计算机上存储浮点数值是按照IEEE(电气和电子工程师协会)754浮点存储格式标准来存储的。

IEEE单精度浮点格式共32位,包含三个构成字段:23位小数f8位偏置指数e1位符号s。将这些字段连续存放在一个32位字里,并对其进行编码。其中0:22位包含23位的小数f 23:30位包含8位指数e;第31位包含符号s。如下图所示:

也就是说上面将0.650.5转换出的二进制代码,我们只能存储23位,即使数据类型为double,也只能存储52位,这样大家便能看出问题出现的原因了。

截取的二进制代码已无法正确表示0.650.5,根据这个二进制代码肯定无法正确得到结果0.05

decimalfloatdouble错误的认识

引用自:http://topic.csdn.net/t/20050514/20/4007155.htmlIvony的评论

在精确计算中使用浮点数是非常危险的,尽管C#在浮点数运算时采取了很多措施使得浮点数运算的结果看起来是非常正常的。但实际上如果不清楚浮点数的特性而贸然使用的话,将造成非常严重的隐患。

考虑下面的语句:
double dd = 10000000000000000000000d;
dd += 1;
Console.WriteLine ( "{0:G50}", dd );
输出是什么?谁知道?
输出是:1000000000000000000000000
这就是浮点数精度损失的问题,最重要的是,在精度损失的时候,不会报告任何的错误,也不会有任何的异常产生。
浮点数的精度损失可能在很多地方出现,例如d * g / g不一定等于dd / g * g也不一定等于d
还有两个非常危险的错误认识!!
1
decimal不是浮点型、decimal不存在精度损失。下面有段程序大家可以去看看结果是什么。记住!所有的浮点型变量都存在精度损失的问题,而decimal是一个不折不扣的浮点型,不论它精度有多高,精度损失依然存在decimal dd = 10000000000000000000000000000m;
dd += 0.1m;
Console.WriteLine ( "{0:G50}", dd );

2decimal所能储存的数比double大,从doubledecimal的类型转换不会出现任何问题。微软在decimal的帮助上真的要好好反省了。实际上只有从整形到decimal的转换才是扩大转换,decimal的精度比double大,但所能储存的最大数却比double要小。



计算机中的二进制的小数同十进制一样,比如10.125可以写成1.125*10的一次方,二进制写成1010.001,1.010001*2的三次方,都可以写成谁乘以进制的指数次,所以,在IEEE规则中规定浮点数的构成是符号位S+指数位E+尾数M,这个位数的组合构成浮点数。

JAVA遵守IEEE754规则:

float占4个字节,32位,表示为SEEEEEEEEMMMMMMMMMMMMMMMMMM,1位符号位,8位指数位,23位尾数位。当然如果指数值在1-254之间时,为1.M,否则就是0.M,指数又叫阶码,阶码采用移码表示,取值E-127.所以公式可总结为:小于0的小数:(-1)的S次*2的(1-127)次*(0.M),大于0的小数:(-1)的S次*2的(E-127)*(1.M).

5.0的在JAVA中的计算机二进制流表示为:

S=0,

5.0=101=1.01*2的2次

E-127=2 E=129=10000001

M=1.01

组合在一起就是 0 10000001 01000000000000000000000

再转成十进制:(-1)的0次 * 2的(129-127)次*(1.01)=101=5.0

输出float的二进制流,Java中的方法Float.floatToIntBits()

-5.0的表示:

Java二进制流:1 10000001 01000000000000000000000,其他省略。

0.1的表示:

0.1约等于0.0001100110011001100110011001=1.100110011001100110011001*2的-4次

S=0

E-127=-4 E=123=01111011

M=10011001100110011001100

组合在一起在JAVA中二进制流就是0 01111011 100110011001100110011001100

在转换成10进制为0.100000001490116119384765625

误差为0.000000001490116119384765625 如果26个float的0.1相加,结果为2.5999997 如果定义26个double的0.1相加,结果为2.600000000000001.

所以可以得出:

1.浮点数强制转换成整数时,舍入误差严重加重,强制转换时会舍弃非整数部分。

double d=29*0.01;//d=0.29

d=((int)d*100);//d=28

2.科学计数表示法中二进制位数越多,精度就越高。64位double就比32位的float精度高。对精度要求非常高的场合可以用BigDecimal类,能表示任意精度的数。

3.指数E为255时表示特殊数字。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值