这里写目录标题
- 负数的补码存储
- 十进制浮点数与二进制的转换
- 有限循环的二进制
- 无限循环的二进制
- 计算机对浮点数的保存
- 无限循环二进制数的保存
- 浮点数的近似
- 参考文献
负数的补码存储
首先我们回忆一下负数的补码表示。我们都知道,有符号数的负数使用补码的方式进行存储:
之所以这样,就是 方便统一运算 ,如果负数不是使用补码的方式表示,则在做基本对加减法运算的时候, 还需要多一步操作来判断是否为负数,如果为负数,还得把加法反转成减法,或者把减法反转成加法 ,这就非常不好了,毕竟加减法运算在计算机里是很常使用的,所以为了性能考虑,应该要尽量简化这个运算过程。
十进制浮点数与二进制的转换
有限循环的二进制
这种最简单的情况,直接遵照我们的默认转换方法:
- 整数部分采用:除 2 取余法
- 小数部分采用:乘 2 取整法
其计算过程如下图所示:
反之二进制转换为十进制则较为简单,按位乘以2的对应幂次即可:
无限循环的二进制
上面所说的是可以用有限位二进制表示的十进制数,但是还有的数字是无法用有限位二进制来表达的,它们转换的过程中变成了无限循环的二进制。
例如按照之前的算法,对于 0.1
,转换为二进制,过程如下:
可以发现,0.1
的二进制表示是无限循环的。
由于计算机的资源是有限的,所以是没办法用二进制精确的表示 0.1,只能用「近似值」来表示,就是在有限的精度情况下,最大化接近 0.1 的二进制数,于是就会造成精度缺失的情况 。在后面会介绍0.1这种无限循环二进制数是如何在计算机中保存的。
计算机对浮点数的保存
现在绝大多数计算机使用的浮点数,一般采用的是 IEEE 制定的国际标准,这种标准形式如下图:
这三个重要部分的意义如下:
- 符号位:表示数字是正数还是负数,为 0 表示正数,为 1 表示负数;
- 指数位:指定了小数点在数据中的位置,指数可以是负数,也可以是正数,指数位的长度越长则数值的表达范围就越大;
- 尾数位:小数点右侧的数字,也就是小数部分,比如二进制 1.0011 x 2^(-2),尾数部分就是 0011,而且尾数的长度决定了这个数的精度,因此如果要表示精度更高的小数,则就要提高尾数位的长度;
用 32
位来表示的浮点数,则称为单精度浮点数,也就是我们编程语言中的 float
变量,而用 64
位来表示的浮点数,称为双精度浮点数,也就是 double
变量,它们的结构如下:
2进制小数转换为2进制浮点数保存的步骤如下:
可以注意到转换过程中:
- 首先要移动小数点到第一个有效数字后面,然后小数点右侧的数字就是浮点数里的尾数位存储的值。
- 指数位在存储时以+127的方式进行;这样就可以把指数转换成无符号数,可以表达的指数取值范围在
-126 ~ +127
。 - 移动后的小数点左侧的有效位(即 1)消失了,因为 IEEE 标准规定默认左侧最高位就是1
因此转换公式为:
我们举个例子,例如将 -5.125
转换成float类型进行保存,则按照IEEE 754标准的规则进行以下步骤:
- 符号位:由于-5.125是负数,符号位为1。
- 绝对值的二进制表示:将5.125的绝对值转换为二进制形式,得到101.001。
- 规范化:将二进制表示规范化,即将小数点左移,直到只有一位非零数字位,得到1.01001。
- 指数位:计算小数点左移的位数,这里是2。
- 偏移量:对于32位浮点数,偏移量为127。将实际指数值2加上偏移量,得到指数位的值为129。
- 最终二进制表示:将符号位、指数位和尾数位按顺序连接起来,得到最终的32位二进制表示: 1 10000001 01001000000000000000000
我们编写代码进行测试,这里使用联合体(union)来转换浮点数和二进制数据:
#include <iostream>
#include <iomanip>
union FloatBinary {
float f;
unsigned int i;
};
int main() {
float num = -5.125;
FloatBinary fb;
fb.f = num;
// 将二进制表示以字符串形式打印
std::cout << num << "\t-->\t";
for (int i = 31; i >= 0; --i) {
std::cout << ((fb.i >> i) & 1);
if (i == 31 || i == 23) {
std::cout << ' ';
}
}
std::cout << std::endl;
return 0;
}
测试结果如下:
和我们上面按照默认步骤计算的结果是一致的。
无限循环二进制数的保存
上面提到 0.1
这种无限循环的二进制数,对于这种数字,计算机是如何保存的呢?
使用 binaryconvert
这个工具,将十进制 0.1 小数转换成 float 浮点数,观察如下:
可以看到,8 位指数部分是 01111011
,23 位的尾数部分是 10011001100110011001101
,可以看到 尾数部分是 0011
是一直循环的 ,只不过尾数是有长度限制的,所以只会显示一部分,所以是一个近似值,精度十分有限。
我们用刚刚的程序在代码中观察一下,发现和显示的一致:
再看看 0.2
的保存方式:
可以看到,8 位指数部分是 01111100
,稍微和 0.1 的指数不同,23 位的尾数部分是 10011001100110011001101
和 0.1 的尾数部分是相同的,也是一个近似值。
再用代码观察,发现也是一致的:
浮点数的近似
我们再将两个浮点数转换回十进制:
这两个结果相加就是 0.300000004470348358154296875
:
可以得出,计算机里对这样的浮点数采取近似保存,所以其相加得出的也是一个近似数。我们用代码测试一下:
#include <iostream>
#include <iomanip>
int main() {
double num1 = 0.1f;
double num2 = 0.2f;
double sum = num1 + num2;
std::cout << std::setprecision(32);
std::cout << "num1:\t\t" << num1 << std::endl;
std::cout << "num1:\t\t" << num2 << std::endl;
std::cout << "num1+num2:\t" << sum << std::endl;
return 0;
}
至于这里为什么将三个数据类型都设定为了double,是因为float的时候,相加的结果并不会等于我们预想的值:
之所以这样,是因为当进行相加操作时,0.1和0.2的近似值参与计算,由于浮点数精度有限,可能 会产生进一步的近似和舍入误差 。因此,相加的结果变成了 0.300000011920928955078125
,与我们期望的精确结果0.3有微小的差异。
参考文献
- 2.7 为什么 0.1 + 0.2 不等于 0.3 ? | 小林coding
部分图片来源网络,如有侵权请联系我删除。
如有疑问或错误,欢迎和我私信交流指正。
版权所有,未经授权,请勿转载!
Copyright © 2023.07 by Mr.Idleman. All rights reserved.