浮点数作为近似值和使用误差
浮点数作为近似值,以及理解使用单精度和双精度数据类型时发生的误差
CFloat.vb 是一个用于在VB中处理和解释浮点数的类。
什么是浮点数
计算机使用浮点数来处理带有小数点的实数。在某些情况下,我们想要表示的数字不能完全由浮点二进制数字序列的角色来精确表示。 因此,浮点数的数学运算可能会产生与我们预期略有不同的结果。 例如:在C#,VB,VBA,Python,PHP和Java中计算0.43 + 0.000001 - 0.430001,将不会返回0 !!
存储图
标准IEEE 754-2019 / ISO IEC 60559:2020 旨在组织浮点运算的轮廓。
[Float / Single]数据类型存储在4个字节= 32位中,如下面的二进制存储图所示。 S是信号位,E是指数位,F是尾数位。
尾数也称为有效数字。
单精度数字的二进制格式如下
S EEEE EEEE FFF FFFF FFFF FFFF FFFF FFFF
内存 十六进制 | 信号 位 | 指数 十六进制 | 尾数 十六进制 | |
Epsilon 最小正数 1.4E-45 | 0000 0001 | 0 | 00 | 000 0001 |
零; +0! | 0000 0000 | 0 | 00 | 000 0000 |
负零; -0! | 8000 0000 | 1 | 00 | 000 0000 |
1! | 3F80 0000 | 0 | 7F | 000 0000 |
大于1的最小数 1.00000012! | 3F80 0001 | 0 | 7F | 000 0001 |
不是数字; NaN | FFC0 0000 | 1 | FF | 400 0000 |
无穷大; ∞ | 0F80 0000 | 0 | FF | 000 0000 |
负无穷大; -∞ | FF80 0000 | 1 | FF | 000 0000 |
双精度数字的二进制格式如下所示
S EEEE EEEE EEE FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF
内存 十六进制 | 信号 位 | 指数 十六进制 | 尾数 十六进制 | |
Epsilon 最小正数 5E-324 | 0000 0000 0000 0001 | 0 | 000 | 0 0000 0000 0001 |
零; +0# | 0000 0000 0000 0000 | 0 | 000 | 0 0000 0000 0000 |
负零; -0# | 8000 0000 0000 0000 | 1 | 000 | 0 0000 0000 0000 |
1# | 3FF0 0000 0000 0000 | 0 | 3FF | 0 0000 0000 0000 |
大于1的最小数 1.0000000000000002# | 3FF0 0000 0000 0001 | 0 | 3FF | 0 0000 0000 0001 |
不是数字; NaN | FFF8 0000 0000 0000 | 1 | 7FF | 8 0000 0000 0000 |
无穷大; ∞ | FF00 0000 0000 0000 | 0 | 7FF | 0 0000 0000 0000 |
负无穷大; -∞ | FFF0 0000 0000 0000 | 1 | 7FF | 0 0000 0000 0000 |
由浮点数引起的意外代码运行
由于浮点数是实数的近似值,因此加法和数学运算并不总是能得到精确的结果。
加法测试
//C#
public void TestAdd()
{
double V = 0;
V = 0.43 + 1E-06 - 0.430001;
if (V != 0) {
Interaction.MsgBox("It will go here!");
}
float f = 0;
f = 0.43f + 1E-06f - 0.430001f;
if (f != 0) {
Interaction.MsgBox("It will go here!");
}
}
'VB
Sub TestAdd()
Dim V As Double
V = 0.43# + 0.000001# - 0.430001#
If V <> 0 Then
MsgBox("It will go here!")
End If
Dim f As Single
f = 0.43! + 0.000001! - 0.430001!
If f <> 0 Then
MsgBox("It will go here!")
End If
End Sub
特殊值
'VB
Sub Test(x As Double)
If x > 0 Then
MsgBox("X > 0")
ElseIf x = 0 Then
MsgBox("X = 0")
ElseIf x < 0 Then
MsgBox("X < 0")
Else
MsgBox("This is a possible case! What is the value of X hear?")
Dim R =
(Double.NaN = 0) = False AndAlso
(Double.NaN < 0) = False AndAlso
(Double.NaN > 0) = False
End If
End Sub
Sub Test(x As Single)
If x > 0 Then
MsgBox("X > 0")
ElseIf x = 0 Then
MsgBox("X = 0")
ElseIf x < 0 Then
MsgBox("X < 0")
Else
MsgBox("This is a possible case! What is the value of X hear?")
Dim R =
(Single.NaN = 0) = False AndAlso
(Single.NaN < 0) = False AndAlso
(Single.NaN > 0) = False
End If
End Sub
//C#
public void Test(double x)
{
if (x > 0) {
Interaction.MsgBox("X > 0");
} else if (x == 0) {
Interaction.MsgBox("X = 0");
} else if (x < 0) {
Interaction.MsgBox("X < 0");
} else {
Interaction.MsgBox("This is a possible case! What is the value of X hear?");
dynamic R = (double.NaN == 0) == false && (double.NaN < 0) == false &&
(double.NaN > 0) == false;
}
}
public void Test(float x)
{
if (x > 0) {
Interaction.MsgBox("X > 0");
} else if (x == 0) {
Interaction.MsgBox("X = 0");
} else if (x < 0) {
Interaction.MsgBox("X < 0");
} else {
Interaction.MsgBox("This is a possible case! What is the value of X hear?");
dynamic R = (float.NaN == 0) == false &&
(float.NaN < 0) == false && (float.NaN > 0) == false;
}
}
如何从指数和尾数获取浮点数值
以下函数将获取浮点数的double
值。浮点数的指数偏差和尾数基数取决于浮点数的类型。 见表。
类型 | 总位数 | 指数偏差 | 尾数基数 |
半精度 / Float16 | 16 | 15 | 2^10 |
单精度 / Float | 32 | 127 | 2^23 |
双精度浮点型 | 64 | 1023 | 2^52 |
四精度 / Float128 | 128 | 16383 | 2^112 |
一个数字的值由以下公式计算得出
指数 <> 0
值 = ±2^(指数 - 指数偏差) * (1 + 尾数 / 尾数基数)
指数 = 0
值 = ±2^(1- 指数偏差) * (尾数 / 尾数基数)
'VB
Function GetDoubleValue(IsNegative As Boolean, Exponent As UInt16,
Fraction As UInt64, ExponentBias As UInt16, FractionBase As UInt64) As Double
If Exponent = 0 Then
If Fraction = 0 Then
Return If(IsNegative, -0#, 0#)
End If
Dim FractionRatio = Fraction / FractionBase
Return If(IsNegative, -1#, 1#) * (2 ^ (1 - ExponentBias)) * FractionRatio
Else
If Exponent = 2 * ExponentBias + 1 Then
If Fraction = 0 Then
Return If(IsNegative, Double.NegativeInfinity, Double.PositiveInfinity)
Else
Return Double.NaN
End If
End If
Dim FractionRatio = Fraction / FractionBase
Return If(IsNegative, -1#, 1#) * _
(2 ^ (CInt(Exponent) - ExponentBias)) * (1# + FractionRatio)
End If
End Function
//C#
public double GetDoubleValue(bool IsNegative, UInt16 Exponent, UInt64 Fraction,
UInt16 ExponentBias, UInt64 FractionBase)
{
if (Exponent == 0) {
if (Fraction == 0) {
return IsNegative ? -0.0 : 0.0;
}
dynamic FractionRatio = Fraction / FractionBase;
return IsNegative ? -1.0 : 1.0 *
(Math.Pow(2, (1 - ExponentBias))) * FractionRatio;
} else {
if (Exponent == 2 * ExponentBias + 1) {
if (Fraction == 0) {
return IsNegative ? double.NegativeInfinity : double.PositiveInfinity;
} else {
return double.NaN;
}
}
dynamic FractionRatio = Fraction / FractionBase;
return IsNegative ? -1.0 : 1.0 *
(Math.Pow(2, (Convert.ToInt32(Exponent) - ExponentBias))) *
(1 + FractionRatio);
}
}
数据库主键和浮点数
由于浮点数是实数的近似值; 因此,将其用作数据库中的主键不是一个好主意。
例如
如果我们有一个名为Customers
的表,其中包含一个数据类型为single的字段id
,那么即使数据库中存在记录,引用一行也可能导致没有记录。
当我们插入0.4301作为id时,实际的id会略有不同,因此可能会在数据库update
或select
中产生意外的结果。
下面的SQL可能会导致没有记录,这取决于数据库驱动程序以及它如何将数字从十进制转换为浮点(float)类型。
Select Customer From Customers where id = 0.4301