计算机中的浮点数只是无限接近真实值的近似值。为什么呢?首先来看一下浮点数在计算机中是如何存储的。

浮点数的存储        

计算机中浮点数的存储遵循IEEE754浮点数标准。单精度用32位存储,而双精度用64位存储。具体的存法如下(以单精度为例):

单精度(32位) = 1位符号位+8位指数位+23位尾数位,见下图。

单精度的浮点运算是什么意思 单精度浮点数怎么计算_单精度的浮点运算是什么意思

其中,符号位用来表示数值的符号。关于指数位和尾数位,需要先了解计算机表示浮点数的原理。计算机中,浮点数是二进制科学计数法来表示的。比如10.5(十进制)=1010.1(二进制)=1.0101 * 2^3.那这里1.0101就叫尾数,3就叫指数。这两个值会分别存入浮点数的指数和尾数区域。要注意的是:

1. 因为尾数第一位肯定为1,所以在存储的时候省略,在读取数据的时候在加上。这样会将尾数部分实际的存储能力扩大到24位。

2. 指数也有可能为负数,8位只能表示-127到128。在存储指数的时候实际是存储的原数据+127的值,读取指数数据的时候再减去127。例如:指数3,实际存储的值是3+127 = 130

指数和尾数的存储能力直接决定了其能表示数字的范围和精度:

1. 数字范围:(-1)*2^(符号位) * 尾数最大数(1.1111...) * 指数最大数(2^128) = (-3.4 * 10^38, 3.4 * 10^38)

2. 精度:尾数最多能存24位,最大值为2^24 = 16777216  这个数字能完整表示 10^7以内的整型数字,因此精度为7位

C# float类型参考

浮点数的精度误差    

 小数的二进制表达:前面其实提到过,10.5的二进制是1010.1,整数部分的算法是除二取余(余数为当前位的二进制值)。小数部分的算法是乘二取整数部分作为当前位的二进制值,小数部分继续进行乘二取整。所以10.5的二进制表达为1010.1。

但是现实情况是大部分小数乘2取整的最后是一个无限循环。比如:0.99,可以一直执行乘二取整,最后也不会得到一个乘二后值为1的数字。所以这种值会变为二进制时是无法存储其精确值的,因为尾数的存储位数是有限制的。(这里的单精度为23位) 因此会有精度丢失,当从内存中取值时得到的数值其实是无限接近于真实值的近似值。

比如执行如下代码:

float fValue = 0.99F;
Console.WriteLine("float:" + fValue.ToString("G8"));
// output: 0.99000001

原因是什么呢?运用上面的存储原理可以做如下解释:0.99转换为二进制后的值为(只列出前25位):1111110101110000101000111(25bit),但是单精度只能存24位,那实际存储的值为:111111010111000010100011(24bit)。这里就丢掉了一位数据。当还原为10进制数时就会不够准确。其实这里我尝试还原了一下,但是得到的结果和预期不太一样(结果为:0.9899999499320983886716),可能计算过程中本身也有误差导致。还原代码如下:

static void BinToDecimal(string binString)
{
            decimal result = 0;
            int index = -1;
            foreach (var c in binString)
            {
                if (c == '1')
                {
                    result += (decimal)Math.Pow(2, index);
                }

                index -= 1;
            }

            Console.WriteLine(result);
}

上述代码如果我们这样写:

float fValue = 0.99F;
Console.WriteLine("float:" + fValue.ToString());
// output:0.99

结果就是对的,原因是fValue.ToString() 默认是显示7位精度。这样得到的结果就是0.99。

各种数据类型的ToString()方法默认显示精度见MSDN

Decimal类型        在财务计算等高精度要求的场景,浮点数的误差可能会造成严重后果。所以decimal类型适用于这种高精度的场景。decimal的存储方式是:

符号位(1bit) + 数值位(96bit) + 放大因子(31bit) = 128bit

其中数值位用来存储要表示的完整数字去掉小数点后的整数表示,放大因子决定小数点的位置。因此所能存储的数字范围为(-2^96,2^96)对应十进制:(-79228162514264337593543950336,79228162514264337593543950336).所以decimal一共是能表示精度28位的数字。不存在十进制与二进制的转换,所以不会有精度误差。所以对数值精度要求高时推荐使用decimal数据类型。