0、引言
本文为作者读程序是怎样跑起来的一书第三章的读书笔记。
1、将0.1累加100次也得不到10
看下面一段程序:
#include <stdio.h>
void main()
{
float sum;
int i;
sum = 0;
// 0.1相加100次
for (i = 1; i <= 100; i++) {
sum += 0.1;
}
printf("%f\n", sum);
}
复制代码
上面的程序功能是把0.1累加100次后,然后将累加后的值打印出来。我们知道,这个预期值应该是10。但是运行结果并不是10。
// 运行结果:
10.000002
复制代码
那这段程序运行出错的原因是什么呢?我们知道,计算机内部存储的信息都是以二进制的形式表示出来的。所以小数在计算机内部也是由二进制表示的。但是”有一些十进制的小数无法转换成二进制数”。十进制数0.1就是这种情况,转换成二进制后变成0.00011001100…(1100训话)(就像无法用十进制数表示1/3一样。1/3就是0.3333…)。
2、用二进制数表示小数
用二进制数表示小数只需将各数位数值和位权相乘,然后再将相乘的结果相加即可。
二进制数小数点前面部分的位权,第1位是2的0次幂,第2位是2的1次幂……以此类推。小数点后面部分的位权,第1位是2的-1次幂,第2位是2的-2次幂,以此类推。
比如二进制数1011.0011表示的十进制数就是:1 * 2^3 + 0 * 2^2 + 1 * 2^1 + 1 * 2^0 + 0 * 2^-1 + 0 * 2^-2 + 1 * 2^-3 + 1 * 2^-4 = 11.1875
3、什么是浮点数
像1011.0011这样带小数点的二进制表现形式,完全是我们纸面上的二进制表现形式,在计算机内部是无法使用的。
计算机内部是用浮点数来表示的。比如C语言中有双精度浮点数和单精度浮点数。双精度浮点数类型用64位,单精度浮点数类型用32位来表示全体小数。
浮点数是指用符号、尾数、基数和指数这四部分来表示的小数。因为计算机内部使用的是二进制数,所以基数是2.因此,实际的数据中往往不考虑基数,只用符号、尾数、指数这三部分即可表示浮点数。
// 浮点数的表现形式
±m*n^e
其中±是符号,m是尾数,n是基数,e是指数
复制代码
浮点数的表现方式有很多种,最为普遍的是IEEE标准:
符号部分是指使用一个数据位来表示数值的符号。该数据位是1表示负,0表示正。
尾数部分用的是”将小数点前面的值固定为1的正则表达式”。
指数部分用的则是”EXCESS系统表现”。
3.1、正则表达式
尾数部分用的是正则表达式。具体是说,在二进制数中,我们使用的是”将小数点前面的值固定为1的正则表达式“。也就是说,将二进制数表示的小数左移或者右移(逻辑移位,因为符号位是独立的)数次后,整数部分的第1位是1,第2位之后都是0。而且,第1位的1在实际的数据中不保存。由于第1位必须是1,因此,省略该部分后就节省了一个数据位,从而也就可以表示更多的数据范围。
比如,二进制数1011.0011的单精度浮点数尾数部分的正则表达式就是如下的表示方法:
1011.0011 // 原始数据
|
0001.0110011 // 右移使整数部分的第1位变成1
|
0001.0110011000000000000000 // 确保小数点以后的长度为23位
|
0110011000000000000000 // 仅宝轮小数点后面的部分,完成正则表达式
复制代码
3.2、EXCESS系统
EXCESS系统是指,通过将指数部分表示范围的中间值设为0,使得负数不需要用符号位来表示。也就是说,当指数部分是8位单精度浮点数时,最大值为11111111=255的1/2,即01111111 =127(小数部分舍弃)表示的是0。指数部分是11位双精度浮点数时,1111111111=2047的1/2即0111111111=1023(小数部分舍弃)表示的是0。
上面说的可能不太好理解。举个例子。假设有这样一个游戏。用113(AK)的扑克牌来表示负数。这时,我们可以把中间的7这张牌当成0,。如果扑克牌7是0,则扑克牌10就表示+3,扑克牌3就表示-4。这个就是EXCESS系统。
4、在实际的程序中进行确认
#include <stdio.h>
#include <string.h>
// 函数功能:用于确认单精度浮点数表示方法
void main()
{
float data;
unsigned long buff;
int i;
char s[34];
// 将0.75以单精度浮点数的形式存储在变量data中
data = (float)0.75;
// 把数据复制到4字节长度的整数变量buff中以逐个提取出每一位
memcpy(&buff, &data, 4);
// 逐一取出每一位
for (i = 33; i >=0; i--) {
if (i == 1 || i == 10) {
// 加入破折号来区分符号部分、指数部分和尾数部分
s[i] = '-';
} else {
// 为各个字节赋值'0'或者'1'
if (buff % 2 == 1) {
s[i] = '1';
} else {
s[i] = '0';
}
buff /= 2;
}
}
s[34] = '\0';
printf("%s\n", s);
}
复制代码
该程序执行后,运行结果是:
0-01111110-10000000000000000000000
复制代码
也就是说十进制数0.75用单精度浮点数来表示就变成了0-01111110-10000000000000000000000
这里,符号位为0.指数部分为01111110,是十进制的126,用EXCESS系统表现就是-1(126-127)。尾数部分是10000000000000000000000,实际上表示的是1.10000000000000000000000这个二进制数,换成十进制数就是1.5。
因此,0-01111110-10000000000000000000000这个单精度浮点数,表示的就是”1.5 * 2的-1次幂”,即+1.5*0.5 = +0.75。正好吻合,结果正确。
将程序中的0.75换成0.1.就得到了用单精度浮点数表示十进制数0.1。结果是0-01111011-10011001100110011001101。但是如果反过来计算这个数值的十进制,你会发现居然不是0.1。这就是刚开始说0.1累加100次得不到10的原因。
data = (float)0.75;
复制代码
5、如何避免计算机计算出错
- 回避策略,即无视这些错误。
- 把小数转换成整数来计算。(因为整数计算一定不会出错)
例如,将0.1相加100次的计算,可以转换成0.1扩大10倍后再将1相加100次的计算,最后把结果除以10就可以了。
#include <stdio.h>
// 函数功能:用于确认单精度浮点数表示方法
void main()
{
int sum;
int i;
sum = 0;
for (i = 1; i <= 100; i++) {
sum += 1;
}
// 总和结果除以10
sum /= 10;
printf("%d\n", sum);
}
复制代码