float、double浮点数运算为啥会丢失精度?

本文揭示了浮点数(float/double)精度损失的原理,通过科学计数法解释了为何会出现误差,并介绍了如何用BigDecimal解决精度问题。重点讲解了BigDecimal的原理和使用场景,以及为何其适用于精确计算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:在工作中,谈到有小数点的加减乘除都会想到用BigDecimal来解决,但是有很多人对于double或者float为啥会丢失精度一脸茫然。还有BigDecimal是怎么解决的?话不多说,我们开始。

1.浮点数是啥?

 浮点数是计算机用来表示小数的一种数据类型,采用科学计数法。在java中,double是双精度,64位,浮点数,默认是0.0d。float是单精度,32位.浮点数,默认是0.0f;

 在内存中存储

ac189484da6532ca2c14a25665bc97072b9.jpg

float      符号位(1bit)   指数(8 bit)     尾数(23 bit)
double   符号位(1bit)  指数(11 bit)   尾数(52 bit)


float在内存中指数是8bit,由于阶码实际存储的是指数的移码,假设指数的真值是e,阶码为E,则有E=e+(2^n-1 -1)。其中 2^n-1 -1是IEEE754标准规定的指数偏移量,根据这个公式我们可以得到 2^8 -1=127。于是,float的指数范围为-128   +127,而double的指数范围为-1024  +1023。其中负指数决定了浮点数所能表达的绝对值最小的非零数;而正指数决定了浮点数所能表达的绝对值最大的数,也即决定了浮点数的取值范围。


float的范围为-2^128 ~ +2^127,也即-3.40E+38 ~ +3.40E+38;
double的范围为-2^1024 ~ +2^1023,也即-1.79E+308 ~ +1.79E+308

2.走进失真之科学计数法

 我们先说说科学计数法,科学计数法是一种简化计数的方法,用来近似表示一个极大或极小且位数较多的数,对于位数较小的数值,科学计数法没有什么优势,但对于位数较多的数值其计数方法的优势就非常明显了。例如:光的速速是300000000米/秒,全世界人口数大约是6100000000。类似光的速度和世界人口数这样大数值的数,读、写都很不方便,所以光的速度可以写成3*10^8,全世界人口数可以写成6.1*10^9。所以计算器用科学计数法表示光速是3E8,世界人口数大约是6.1E9。

我们小时候玩计算器喜欢疯狂的累加或者累减,到最后计算器就会显示下图。这个就是科学计数法显示的结果 

c3bf5d2b1869e6754caa30e4f11c02a81cb.jpg

那图中真实的值是  -4.86*10^11=-486000000000。十进制科学计数法要求有效数字的整数部分必须在【1,9】区间内。

 

3.走进失真之精度

计算机在处理数据都涉及到数据的转换和各种复杂运算,比如,不同单位换算,不同进制(如二进制十进制)换算等,很多除法运算不能除尽,比如10÷3=3.3333.....无穷无尽,而精度是有限的,3.3333333x3并不等于10,经过复杂的处理后得到的十进制数据并不精确,精度越高越精确。float和double的精度是由尾数的位数来决定的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响。float:2^23 = 8388608,一共七位,由于最左为1的一位省略了,这意味着最多能表示8位数: 28388608 = 16777216 。有8位有效数字,但绝对能保证的为7位,也即float的精度为7~8位有效数字;double:2^52 = 4503599627370496,一共16位,同理,double的精度为16~17位

731e52b8ffaa28fdf67b3e97b0c18969d2f.jpg
 

当到达一定值自动开始使用科学计数法,并保留相关精度的有效数字,所以结果是个近似数,并且指数为整数。在十进制中小数有些是无法完整用二进制表示的。所以只能用有限位来表示,从而在存储时可能就会有误差。对于十进制的小数转换成二进制采用乘2取整法进行计算,取掉整数部分后,剩下的小数继续乘以2,直到小数部分全为0。

如遇到 

ea0a68703cbec84b967ed448a06acf9415a.jpg

输出是 0.19999999999999998

double类型 0.3-0.1的情况。需要将0.3转成二进制在运算


0.3 * 2 = 0.6 => .0 (.6)取0剩0.6
0.6 * 2 = 1.2 => .01 (.2)取1剩0.2
0.2 * 2 = 0.4 => .010 (.4)取0剩0.4
0.4 * 2 = 0.8 => .0100 (.8) 取0剩0.8
0.8 * 2 = 1.6 => .01001 (.6)取1剩0.6
.............

91e5aa515fd1299d321d85b838adbddd484.jpg

 

 

3.总结

  看完上面,大概清楚了为啥浮点数会有精度问题。简单来说float和double类型主要是为了科学计算和工程计算而设计,他们执行二进制浮点运算,这是为了在广泛的数值范围上提供较为精确的快速近和计算而精心设计的。然而,他们并没有提供完全精确的结果,所以不应该被用于精确的结果的场合。浮点数达到一定大的数会自动使用科学计数法,这样的表示只是近似真实数而不等于真实数。当十进制小数位转换二进制的时候也会出现无限循环或者超过浮点数尾数的长度。
 

4.那我们怎么用BigDecimal来解决?

大家看下面的两个输出

4dad3f8db2d9a65b3091fdbb50465a274d1.jpg

输出结果:

0.299999999999999988897769753748434595763683319091796875
0.3

图上阿里的代码约束插件已经标注警告,让我使用String参数的构造方法创建BigDecimal。因为double不能精确地表示为0.3(任何有限长度的二进制),构造方法传递的值也是不完全等于0.3。大家在使用BigDecimal的时候一定要用String参数的构造方法来创建。说到这里,是木有还有好奇的宝宝有疑问,BigDecimal的原理是啥?为啥它就没有问题呢?其实原理很简单,BigDecimal是不可变的,可以用来表示任意精度的带符号十进制数。double之所以会出问题,是因为小数点转二进制丢失精度。BigDecimal在处理的时候把十进制小数扩大N倍让它在整数上进行计算,并保留相应的精度信息。至于BigDecimal是怎么保存的可以翻阅一下源代码。

博客地址: https://my.oschina.net/wangnian

转载于:https://my.oschina.net/wangnian/blog/3064886

<think>我们正在讨论C#中数值类型使用ToString方法时可能出现的精度丢失问题以及解决方法。根据引用内容,我们知道浮点数float/double)由于二进制表示的限制,在表示某些十进制小数时存在固有精度问题。当使用ToString()方法时,如果不指定格式,可能会显示不确的结果。用户的问题可以分为两部分:1.ToString()方法默认情况下是否会丢失精度?2.如何避免精度丢失?首先,我们需要明确:ToString()方法本身并不会导致额外的精度丢失,但它可能会暴露浮点数在二进制表示中固有的精度问题。例如,0.1在double类型中无法确表示,因此当调用ToString()时,显示的字符串可能不是确的0.1,而是一个接近0.1的值(如0.10000000000000001)。但是,.NET框架的ToString()方法在默认情况下会进行“往返转换”保证:即转换后的字符串如果通过Parse方法转换回原类型,会得到相同的值。这意味着ToString()的输出包含了足够的精度来唯一标识该浮点数的二进制表示。然而,用户往往希望看到的是与原始输入一致的十进制表示,而不是一个很长的字符串。因此,我们需要控制ToString()的格式来达到显示上的要求。根据引用[1][2][3][4]的内容,我们可以总结以下方法:1.指定格式字符串,控制小数位数(如"N4"保留4位小数)[^1]。2.使用字符串传递数值以避免二进制精度问题[^2]。3.使用高精度类型decimal(128位)进行运算转换,因为decimal可以确表示十进制小数[^3][^4]。4.使用decimal.ToString()并指定格式,可以避免浮点数精度问题。因此,针对用户的问题:1.ToString()方法在浮点数上可能会显示由于二进制表示导致的精度问题(看起来像是丢失精度),但实际是浮点数本身的问题。2.避免精度丢失的方法:a)对于浮点数,指定格式字符串,但要注意这并不能解决所有问题(因为浮点数本身就有精度限制)。b)使用decimal类型代替float/double,特别是对精度要求高的场景(如金融计算)。下面我们给出具体示例:示例1:使用double类型,通过指定格式减少显示位数(但无法完全避免精度问题)```csharpdoubled=0.1+0.2;Console.WriteLine(d.ToString("F15"));//显示0.300000000000000Console.WriteLine(d.ToString("F2"));//显示0.30```示例2:使用decimal类型```csharpdecimald1=0.1m;decimald2=0.2m;decimalresult=d1+d2;Console.WriteLine(result.ToString());//显示0.3```另外,引用[3]提到,使用Convert.ToDecimal可以将浮点数转换为decimal,但要注意转换本身可能会引入精度,因为float/double无法确表示某些值。因此,最好一开始就使用decimal类型。总结:-浮点数float/double)的ToString()可能显示不确的十进制表示,这是由浮点数的二进制表示特性决定的。-避免方法:1.如果对精度要求不高,可以指定格式字符串(如"N2"、"F2")来四舍五入到指定位数,但要注意这仅仅是显示效果。2.对于需要确计算的场景,使用decimal类型。注意:decimal类型有更高的精度(28-29位有效数字)更大的存储需求,但运算速度比float/double慢。相关问题:1.如何在C#中正确比较两个浮点数?2.decimal类型在性能敏感场景下的替代方案是什么?3.如何将浮点数格式化为科学计数法并保留精度?注意:在回答中,我们引用了用户提供的引用,并在末尾添加了引用标识。</think>### C# 数值类型 ToString 方法精度丢失问题及解决方案 #### 精度丢失原因 1. **二进制浮点数固有缺陷**: - float/double 类型采用 IEEE 754 二进制浮点表示 - 无法确表示某些十进制小数(如 0.1) - 转换时可能显示多余尾数: ```csharp (0.1 + 0.2).ToString() // 输出 "0.30000000000000004" ``` 2. **默认格式限制**: - 默认 `ToString()` 使用 15 位精度double)或 7 位精度float) - 超出精度的部分会被截断或四舍五入 3. **运算累积误差**: - 浮点运算会放大精度误差[^3] ```csharp double db = 4.0 * 1.3; // 实际存储 5.199999999999999 ``` #### 解决方案 ##### 方法 1:指定格式字符串 ```csharp double value = 0.1 + 0.2; // 保留固定小数位(四舍五入) string s1 = value.ToString("F2"); // "0.30" // 科学计数法确表示 string s2 = value.ToString("E16"); // "3.0000000000000000E-001" // 往返格式(保证可解析回原值) string s3 = value.ToString("R"); // "0.30000000000000004" ``` ##### 方法 2:使用 decimal 类型 ```csharp decimal d1 = 0.1m + 0.2m; // 确十进制表示 string s = d1.ToString(); // "0.3" // 高精度运算示例 decimal result = Convert.ToDecimal(4.0) * Convert.ToDecimal(1.3); // 确 5.2[^3] ``` ##### 方法 3:字符串中间处理 ```csharp double a = 3000.2, b = 3000; double c = a - b; // 理论值 0.2,实际存储可能有误差 // 通过格式化截断误差 string str = c.ToString("N4"); // 保留4位小数 Double.TryParse(str, out c); // 解析后得到确 0.2[^1] ``` ##### 方法 4:自定义格式提供程序 ```csharp public class ExactFormat : IFormatProvider, ICustomFormatter { public object GetFormat(Type formatType) => this; public string Format(string format, object arg, IFormatProvider provider) { if (arg is double d) return d.ToString("G17"); // 17位保证往返安全 return arg.ToString(); } } double num = 0.1 + 0.2; string exact = string.Format(new ExactFormat(), "{0}", num); ``` #### 关键原则 1. **浮点数显示精度**: - double 至少需要 `"G17"` 格式保证往返安全 - float 至少需要 `"G9"` 格式 $$ \text{往返安全条件:} \text{Parse}(\text{ToString}(x)) = x $$ 2. **类型选择建议**: | 类型 | 精度范围 | 适用场景 | |----------|---------------|-----------------------| | double | 15-17 位小数 | 科学计算 | | decimal | 28-29 位小数 | 金融/需要确十进制的场景[^3] | | 字符串 | 无限精度 | 数据传输/存储[^2] | 3. **避免精度丢失实践**: - 金融计算始终使用 `decimal` - 数据传输使用字符串格式 - 显示时明确指定格式: ```csharp // 正确做法 double price = 19.99; displayText = price.ToString("0.00"); ``` #### 特殊场景处理 **科学计数法精度保留**: ```csharp double sci = 0.000000123456789; string exactSci = sci.ToString("E10"); // "1.2345678900E-007" ``` **大整数处理**: ```csharp long bigNum = 1234567890123456789; string safeString = bigNum.ToString("D"); // 完整整数精度 ``` > ⚠️ **重要提示**:浮点数精度限制是**存储层问题**,不是 ToString 的缺陷。ToString 仅反映数值在内存中的实际存储状态[^1][^3]。 --- ### 相关问题 1. 如何确保 C# 中浮点数比较的准确性? 2. decimal 类型在科学计数法格式化时有哪些注意事项? 3. 如何实现自定义的高精度数值类型? 4. 在 JSON 序列化中如何避免浮点数精度丢失? 5. C# 中哪些数值类型转换会导致隐式精度
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值