转载请标明原文地址:https://segmentfault.com/a/1190000041768195
Float二进制表示法
IEEE754标准中规定,单精度浮点数float占4字节32位
Sign(符号1位)|Exponent(指数8位 偏移127)|Mantissa(尾数23位)
Sign(符号):表示浮点数的正负(大于等于0为0,小于0为1)
Exponent(指数):表示浮点数的指数(类似科学计数法的指数部分)
Mantissa(尾数):表示有效数字(类似科学计数法的有效数字)
以下内容简称这3部分分别为S、E、M
S、E、M 这3部分是怎么确定的呢?
比如11.25 表示成十进制的科学计数法: 1.125x101
符号是+,指数是1,有效数字是1.125
IEEE754是先把小数转成二进制,用二进制的科学计数法表示该小数。
还是以11.25为例:
S为0 (因为11.25不为负数)
把浮点数的整数部分和小数部分分别转成二进制,再拼到一起。
整数部分转二进制:
整数部分I为11,转成二进制为:1011
转换方法:整数除以2取余 余数倒序排列(除到整数部分为0)
11 ÷ 2 = 5 ··· 15 ÷ 2 = 2 ··· 12 ÷ 2 = 1 ··· 01 ÷ 2 = 0 ··· 1
小数部分转二进制:
小数部分F为0.25,转成二进制为:.01
转换方法:小数乘以2取整 整数正序排列(乘到小数部分为0,即余数为0)
0.25 x 2 = 0 ··· 0.5
0.5 x 2 = 1 ··· 0
整数和小数拼到一起:
IF = 1011.01
以二进制科学计数法表达:
IF = 1.01101x23
指数部分E:
E = 3, IEEE754规定这个值要加偏移值127
E = 3 + 127 = 130 转成二进制为:10000010
转换方法:整数除以2取余 余数倒序排列(除到整数部分为0)
130 ÷ 2 = 65 ··· 0 65 ÷ 2 = 32 ··· 132 ÷ 2 = 16 ··· 016 ÷ 2 = 8 ··· 08 ÷ 2 = 4 ··· 04 ÷ 2 = 2 ··· 02 ÷ 2 = 1 ··· 01 ÷ 2 = 0 ··· 1
尾数部分M:
M = 101101
由于二进制的科学计数法首位一定为1, 1可以省略不写,尾数部分就变成了01101
M = 01101
S、E、M三部分拼到一起:
0 | 10000010 | 01101 (后面补0够23位)
0 | 10000010 | 0110100 00000000 00000000
11.25转成float二进制最终结果就是:
0 10000010 0110100 00000000 00000000
用《在线浮点数转换工具》验证一下,结果正确。
二进制转Float
二进制11000000110110000000000000000000
转Float怎么转呢?
还是先把二进制分为S、E、M三部分,分别转换后计算最终结果。
S(1位)|E(8位)|M(23位) 即
1 | 10000001 | 1011000 00000000 00000000
符号S: 1(值大于等于0时 S为0 值小于0时 S为1)
指数E:10000001
尾数M:1011000 00000000 00000000
指数E转整数:
E = 1000 0001 转整数
转换方法:从低位到高位 按位转成10进制相加
1x2⁷ + 0x2⁶ + 0x2⁵ + 0x2⁴ + 0x2³ + 0x2² + 0x2¹ + 1x2⁰ =
128 + 0 + 0 + 0 + 0 + 0 + 0 + 1 = 129
减去127(转二进制之前加了指数偏移量127 还原时需要减回来)
E = 129 - 127 = 2
尾数M转小数:
M = 1011
首位补1(为了节省空间 转换时去掉了首位的1 还原时需要补回来)
M = 11011
有效数字 科学计数法
M = 1.1011 x 22
M = 110.11
整数部分 I = 110 小数部分 F = .11
整数部分I转10进制:
I = 110 转成10进制为:6
1x2² + 1x2¹ + 0x2⁰ =
4 + 2 + 0 = 6
I = 6
F = .11 转为float为0.75
1x(½)¹ + 1x(½)² =
1/2 + 1/4 =
0.5 + 0.25 = 0.75
整数和小数部分拼接到一起:
6 + 0.75 = 6.75
加上符号位:
S = 1 (负数 符号为-)
float = -6.75
11000000110110000000000000000000
转成十进制最终结果就是 -6.75
用《在线浮点数转换工具》验证一下,结果正确。
Float特殊值
IEEE754规定了3个特殊值NaN、Infinity、-Infinity
分别代表:非数字(Not a number)、正无穷、负无穷
这个三个特殊值在转换时需要特殊处理
特殊值转换规则:
当指数E全部为1时为特殊值
1.尾数M全部为0
(1)符号位S为0时 值为正无穷Infinity
(2)符号位S为1时 值为负无穷-Infinity
2.尾数M不全部为0
不管符号位S是0还是1 值均为NaN
Float序列化代码
private byte[] bytes = new byte[4];// float转字节数组
private void WriteFloat(float value) {int v = 0;unsafe { v = *(int*)&value; }bytes[0] = (byte)v;bytes[1] = (byte)(v >> 8);bytes[2] = (byte)(v >> 16);bytes[3] = (byte)(v >> 24);
}// 从字节数组中读取float
private float ReadFloat() {uint v = (uint)(bytes[0] | bytes[1] << 8 | bytes[2] << 16 | bytes[3] << 24);float value = float.NaN;unsafe { value = *(float*)&v; }return value;
}private void Test() {WriteFloat(1.23f);float v = ReadFloat();Trace.WriteLine(v);
}
Double二进制表示法
IEEE754标准中规定,双精度浮点数double占8字节64位
S、E、M三部分分别为:
S(1位)|E(11位 偏移1023)|M(52位)
以9.625为例:
S = 0 (正数)
整数部分转二进制:
整数部分 I = 9 转二进制为:1001
转换方法:整数除以2取余 余数倒序排列(除到整数部分为0)
9 ÷ 2 = 4 ··· 1 4 ÷ 2 = 2 ··· 02 ÷ 2 = 1 ··· 01 ÷ 2 = 0 ··· 1
小数部分转二进制:
小数部分F为0.625,转成二进制为:.101
转换方法:小数乘以2取整 整数正序排列(乘到小数部分为0,即余数为0)
0.625 x 2 = 1 ··· 0.250.25 x 2 = 0 ··· 0.50.5 x 2 = 1 ··· 0
整数和小数拼到一起:
IF = 1001.101
以二进制科学计数法表达:
IF = 1.001101 x 23
指数部分E:
E = 3, 加上偏移值1023
E = 1026 转成二进制为:10000000 010
转换方法:整数除以2取余 余数倒序排列(除到整数部分为0)
1026 ÷ 2 = 513 ··· 0513 ÷ 2 = 256 ··· 1256 ÷ 2 = 128 ··· 0128 ÷ 2 = 64 ··· 064 ÷ 2 = 32 ··· 032 ÷ 2 = 16 ··· 016 ÷ 2 = 8 ··· 08 ÷ 2 = 4 ··· 04 ÷ 2 = 2 ··· 02 ÷ 2 = 1 ··· 01 ÷ 2 = 0 ··· 1
尾数部分M:
M = 1001101
由于二进制的科学计数法首位一定为1, 1可以省略不写,尾数部分就变成了001101
M = 001101
S、E、M三部分拼到一起:
S(1位)|E(11位 偏移1023)|M(52位)
0 | 10000000 010 | 0011010... (后面补0够52位)
9.625转成double二进制最终结果就是 0 10000000010 0011010···
用《在线浮点数转换工具》验证一下,结果正确。
二进制转Double
二进制110000000101100100010···
转double怎么转呢?
还是先把二进制分为S、E、M三部分,分别转换后计算最终结果。
1 | 10000000101 | 100100010···
S = 1 是负数 符号为-
E = 10000000101
M = 100100010···
指数E转整数:
E = 1000 0000 101 转整数
转换方法为:从低位到高位 按位转成10进制相加
1x2¹⁰ + 0x2⁹ + 0x2⁸ + 0x2⁷ + 0x2⁶ + 0x2⁵ + 0x2⁴ + 0x2³ + 1x2² + 0x2¹ + 1x2⁰ =
1024 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 4 + 0 + 1 = 1029
减去1023(转二进制之前加了指数偏移值1023 还原时需要减回来)
E = 1029 - 1023 = 6
尾数M转小数:
M = 1001 0001
首位补1(为了节省空间 转换时去掉了首位的1 还原时需要补回来)
M = 1100 1000 1
有效数字 科学计数法
M = 1.10010001 x 26
M = 1100100.01
整数部分 I = 1100100 小数部分 F = .01
整数部分I转10进制:
I = 1100100 转成10进制为:100
转换方法为:从低位到高位 按位转成10进制相加
1x2⁶ + 1x2⁵ + 0x2⁴ + 0x2³ + 1x2² + 0x2¹ + 0x2⁰ =
64 + 32 + 0 + 0 + 4 + 0 + 0 = 100
I = 100
小数部分F转10进制:
F = 01 转成10进制为:0.25
0x(½)¹ + 1x(½)² =
0 + 1/4 =
0 + 0.25 = 0.25
F = 0.25
整数和小数部分拼接到一起:
100 + 0.25 = 100.25
加上符号位:
S = 1 (负数 符号为-)
double = -100.25
二进制110000000101100100010··· 转成十进制最终结果就是 -100.25
用《在线浮点数转换工具》验证一下,结果正确。
Double特殊值
IEEE754规定了3个特殊值NaN、Infinity、-Infinity
分别代表:非数字(Not a number)、正无穷、负无穷
这个三个特殊值在转换时需要特殊处理
特殊值转换规则:
当指数E全部为1时为特殊值
1.尾数M全部为0
(1)符号位S为0时 值为正无穷Infinity
(2)符号位S为1时 值为负无穷-Infinity
1.尾数M不全部为0
不管符号位S是0还是1 值均为NaN
Double序列化代码
private byte[] bytes = new byte[8];private void WriteDouble(double value) {long v = 0;unsafe { v = *(long*)&value; }bytes[0] = (byte)v;bytes[1] = (byte)(v >> 8);bytes[2] = (byte)(v >> 16);bytes[3] = (byte)(v >> 24);bytes[4] = (byte)(v >> 32);bytes[5] = (byte)(v >> 40);bytes[6] = (byte)(v >> 48);bytes[7] = (byte)(v >> 56);
}public double ReadDouble() {int low = bytes[0] | bytes[1] << 8 | bytes[2] << 16 | bytes[3] << 24;int high = bytes[4] | bytes[5] << 8 | bytes[6] << 16 | bytes[7] << 24;long v = (uint)low | (long)high << 32;double value = double.NaN;unsafe { value = *(double*)&v; }return value;
}private void DoubleTest() {WriteDouble(-1.23456d);double v = ReadDouble();Trace.WriteLine(v);
}
总结:
Float转换规则
Sign(符号1位)|Exponent(指数8位 偏移值127)|Mantissa(尾数23位)
Double转换规则
Sign(符号1位)|Exponent(指数11位 偏移值1023)|Mantissa(尾数52位)
特殊值####
NaN、Infinity、-Infinity
if (指数E全部为1) {// 特殊值if (M == 0) {// 判断符号位if (S == 0) {// Infinity } else {// -Infinity}} else {// NaN}
} else {// 正常值
}
IEEE754浮点数常见问题
1.有效数字位数限制
Float单精度浮点数大约6位有效数字
Double双精度浮点数大约15位有效数字
也就是说从左侧第一个不0的数字开始,Float能保证6位有效数字,再往后就不保证有效了
比如123.456f、0.123456f有效,12345.12345f(赋值后等于12345.123)、123456.1234f(赋值后等于123456.125)不能保证有效
Double能保证15位有效数字,再往后就不保证有效了
具体细节可以查看官方文档:
C# 浮点数精度
2.尾数无限循环问题
由于浮点数转换为二进制时,某些数据比如0.1 小数部分乘2以后不能完全乘尽(0.2、0.4、0.8、0.6、0.2、0.4、0.8、0.6... 无限循环) 所以小数会出现丢失精度的情况,不能完全准确的表达这些乘不尽的情况,所以遇到类似精度丢失,数据变大了一点点,变小了一点点的问题,知道是这个原理导致的问题,既不是你写错了,也不是计算机出bug了,完全是转换规则的问题。float类型换成double提高精度依然会有该问题。想解决精度丢失的问题,可以把数字的小数部分当成整数计算 比如a=1.1 b=1.3 计算a+b 可以这样(Math.Round(a10) + Math.Round(b10)) / 10 注意数字别溢出最大范围