浮点数的表示方法类似于我们在日常生活中使用的指数表示法。这是一个数字,使用我们决定足以真实地表示该值的一些数字,我们称之为尾数或有效数字,我们将其乘以一个基数或基数值升高到一个幂,我们称之为指数。简单地说:
num*base^exp
我们通常使用10作为基数,因为我们手上有10个手指,所以我们习惯于像
1e2
这样的数字,它表示
100=1*10^2
。
当然,对于这么小的数字,我们很遗憾地使用指数表示法,但是在处理非常大的数字时,或者更好的是,当我们的数字具有足够代表我们要评估的实体的位数时,我们更喜欢使用它。
正确的位数可能是我们可以用头脑处理的位数,或者是工程应用所需的位数。当我们决定需要多少位数时,我们将不再关心我们即将处理的数字表示与真实值的粘着度。也就是说,对于像
123456.789e5
这样的数字,如果添加
99
个单位,我们就能容忍舍入表示,并且仍然认为它是可接受的,否则我们应该改变表示并使用适当位数的不同表示,例如
12345678900
。
在计算机中,当你需要处理非常大的数字,无法适应标准整数时,或者当你需要表示一个带有小数部分的实数时,正确的选择是使用浮点或双精度浮点表示。它使用与上述讨论相同的布局,但基数为2而不是10。这是因为计算机只有两个手指状态0或1。所以我们之前使用的表示100的公式变成了:
100100*2^0
那仍然不是真正的浮点表示,但可以给出一个想法。现在考虑在计算机中,浮点格式是标准化的,对于标准浮点,根据IEE-754,它使用以下内存布局(我们将在后面看到为什么假定尾数多1位):23位尾数,1位符号和8位指数偏差为-127(这意味着它将在
-126
和
+127
之间变化而无需符号位,并且值
0x00
和
0xff
保留用于特殊含义)。
现在考虑使用0作为指数,这意味着值
2^exponent=2^0=1
乘以尾数会给出与23位整数相同的行为。这意味着像这样递增计数:
float f = 0;
while(1)
{
f +=1;
printf ("%f\n", f);
}
你会发现打印的值线性增加,直到饱和23位,指数将开始增长。
如果我们浮点数的基数或基数为10,那么在前100个(10^2)值中,我们将看到每10个循环增加一次,然后在接下来的1000个(10^3)值中增加100。您可以看到这对应于我们必须进行的截断,因为可用数字数量有限。
使用二进制基数时也会观察到同样的现象,只是更改发生在2的幂间隔上。
到目前为止我们讨论的被称为浮点数的非规范化形式,通常使用的是其对应的规范化形式。后者简单地意味着有一个未存储的第24位始终为1。换句话说,我们不会对小于2^24的数字使用指数为0,但我们会将其移位(乘以2)达到MSbit == 1的24位,然后调整指数到足以强制转换将数字向后移回其原始值的负值。
记得我们之前提到的指数的保留值吗?当 exponent==0x00
时,意味着我们有一个非规格化数。当 exponent==0xff
时,表示一个 nan
(非数字)或者如果 mantissa==0
则为 +/-infinity。
现在应该清楚了,当我们表达的数字超出了24位有效数字(尾数),我们应该期望根据我们距离 2^24
的距离来近似实际值。
现在你正在使用的数字正好处于 2^24=16,277,216
的边缘:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1|0|0|1|0|1|1|0|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1| = 16,277,215
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
s\______ _______/\_____________________ _______________________/
i v v
g exponent mantissa
n
Now increasing by 1 we have:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1|0|0|1|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0| = 16,277,216
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
s\__ exponent __/\_________________ mantissa __________________/
请注意,我们已经将第24位触发为
1
,但从现在开始,我们已经超过了24位表示,每个可能的进一步表示都是以
2^1=2
的步长进行的。只需每次前进2或者可以表示仅为偶数(
2^1=2
的倍数)。也就是说,将最低有效位设置为1,我们有:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1|0|0|1|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1| = 16,277,218
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
s\__ exponent __/\_________________ mantissa __________________/
再次增加:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|1|0|0|1|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0| = 16,277,220
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
s\__ exponent __/\_________________ mantissa __________________/
正如您所看到的,我们无法准确地表示16,277,219。在您的代码中:
// This will print 16777216, because 1 increment isn't enough to
// increase the significant that can express only intervals
// that are > 2^1
printf("16777217 as float is %.1f\n",(float)16777217);
// This will print 16777220, because an increment of 3 on
// the base 16777216=2^24 will trigger an exponent increase rounded
// to the closer exact representation
printf("16777219 as float is %.1f\n",(float)16777219);
如上所述,数字格式的选择必须适合使用情况,浮点数仅是实数的近似表示,并且我们有责任仔细使用正确的类型。
如果需要更高的精度,可以使用双精度或长整型。
为了完整起见,我想补充一下对于不可约分数的近似表示。这些数字不能被2的分数整除,因此在浮点格式中的表示将始终不精确,并且需要在转换为十进制表示时四舍五入到正确的值。
有关更多详细信息,请参见:
在线演示应用程序: