有些脚本语言,比如awk,lua中都是使用浮点数来存储整数的,也就是说,我们在语言中使用的整数,在语言内部是使用浮点数来表示的。我们知道,浮点数的运算中通常存在着一定的误差,所以整数是否能被浮点数精确表示呢?答案是可以,不过不是所有范围的整数都可以被精确的表示。由于浮点数精度的问题,所以浮点数的分布也就呈现出非均匀分布。
先简单介绍一下一个常用的浮点数在计算机中的组成,最常用的浮点数表示法是IEEE754标准,对于这个标准,可以参考[1]
上面就是常用浮点数的表示了,通常sign占用1bit,对于单精度浮点数exponent占用8bits,significand占用23bits。由于IEEE754使用规格化浮点数,所以尾数的最高位必然为1,所以没必要存储,于是有一个隐藏位(hidden bit),所以尾数真正的有效位数为24bits。有兴趣的读者可以参考[1],其中有详细介绍。
当用浮点数来表示整数时,我们要得到连续的整数分布,肯定希望尾数的精度越高越好。很显然,尾数位数有限制,也就限制了能表示的最大的整数的值。对于一个浮点数,设p为其尾数的有效位数(这里p包含一个hidden bit),那么最大的尾数就是1 + 1 - 2^(-p+1),所以最大的整数为(2-2^(-p+1)) * 2^(p-1) = 2^p - 1。由于浮点数是有符号的,所以很自然的我们想到整数范围为[-2^p – 1, 2^p – 1]。可是我们还遗漏了2个数,就是2^p和-2^p。我们来看这两个整数的浮点数表示。规格化后,2^p = (1.0000…000) * 2^p,其中尾数部分小数点后有p个0。但是我们知道,我们的尾数最多只能存储p - 1个,其中一个是hidden bit,但是很巧,由于被舍去的最后一个数字是0,所以不影响实际取值,所以2^p就可以精确表示了。同理-2^p也是如此。
注意,可能有读者会想到,既然1.00..000*2^p可以精确表示,那么很显然,对于单精度浮点数p=24,而指数的可表示范围远大于24,是8-bit,也就是可以到达+127,那么为什么2^p就是可以表示的最大整数呢。因为我们只关心连续的整数范围,2^(p+1)的确可以精确表示,但是[2^p, 2^(p+1)]之间还有许多数就无法表示了,因为尾数不够。
所以对于IEEE754 单精度和双精度浮点数,能够精确表示的整数的范围为
Floating point | Range |
Single precision | [-2^24, 2^24] |
Double precision | [-2^53, 2^53] |
以下代码测试了一下两种浮点数的边界条件(运行环境为VC9):
#include <cassert> int main() { const int max_decimal = 16777216; // 2^24 assert(static_cast<int>(static_cast<float>(max_decimal)) == max_decimal); assert(static_cast<int>(static_cast<float>(max_decimal + 1)) != max_decimal + 1); assert(static_cast<int>(static_cast<float>(-max_decimal)) == -max_decimal); assert(static_cast<int>(static_cast<float>(-max_decimal - 1)) != -max_decimal - 1); const __int64 max_decimald = 9007199254740992; // 2^53 assert(static_cast<__int64>(static_cast<double>(max_decimald)) == max_decimald); assert(static_cast<__int64>(static_cast<double>(max_decimald + 1)) != max_decimald + 1); assert(static_cast<__int64>(static_cast<double>(-max_decimald)) == -max_decimald); assert(static_cast<__int64>(static_cast<double>(-max_decimald - 1)) != -max_decimald - 1); }
EDIT: 修正单精度的范围,应该是2^24。
Reference:
[1] What Every Computer Scientist Should Know About Floating-Point Arithmetic