浮点数值(至少是IEEE754的)基本上有三个组成部分:
精度决定了可以用于指数和尾数的位数。让我们来看一下单精度浮点数的值0.1:
s eeeeeeee mmmmmmmmmmmmmmmmmmmmmmm 1/n
0 01111011 10011001100110011001101
||||||||||||||||||||||+- 8388608
|||||||||||||||||||||+-- 4194304
||||||||||||||||||||+--- 2097152
|||||||||||||||||||+---- 1048576
||||||||||||||||||+----- 524288
|||||||||||||||||+------ 262144
||||||||||||||||+------- 131072
|||||||||||||||+-------- 65536
||||||||||||||+--------- 32768
|||||||||||||+---------- 16384
||||||||||||+----------- 8192
|||||||||||+------------ 4096
||||||||||+------------- 2048
|||||||||+-------------- 1024
||||||||+--------------- 512
|||||||+---------------- 256
||||||+----------------- 128
|||||+------------------ 64
||||+------------------- 32
|||+-------------------- 16
||+--------------------- 8
|+---------------------- 4
+----------------------- 2
符号是正的,很容易理解。
指数为64+32+16+8+2+1 = 123 - 127 bias = -4
,因此乘数为2-4或1/16
。偏置存在是为了使您可以得到非常小(例如10-30)和非常大的数。
尾数很大。它由1
(隐式基数)加上(对于那些每个位都值得1/(2n)的位,其中n
从1
开始增加到右边),{1/2, 1/16, 1/32, 1/256, 1/512, 1/4096, 1/8192, 1/65536, 1/131072, 1/1048576, 1/2097152, 1/8388608}
。
当您将它们全部相加时,您会得到1.60000002384185791015625
。
当您将其乘以2-4乘数时,您将得到0.100000001490116119384765625
,这就是为什么他们说您无法精确表示IEEE754浮点数的0.1
。
关于将整数转换为浮点数,如果您在尾数中有与整数位模式相同数量的位数(包括隐式的1),则可以将整数位模式传输并选择正确的指数。不会失去精度。例如,双精度IEEE754(64位,其中52/53为尾数)没有问题接受32位整数。
如果整数位数更多(例如32位整数和32位单精度浮点数,其仅具有23/24位尾数),则需要缩放整数。
这涉及剥离最低有效位(实际上是舍入),以使其适合于尾数位。当然这会导致精度损失,但无法避免。
让我们看一个具体值123456789
。以下程序转储了每种数据类型的位。
#include <stdio.h>
static void dumpBits (char *desc, unsigned char *addr, size_t sz) {
unsigned char mask;
printf ("%s:\n ", desc);
while (sz-- != 0) {
putchar (' ');
for (mask = 0x80; mask > 0; mask >>= 1, addr++)
if (((addr[sz]) & mask) == 0)
putchar ('0');
else
putchar ('1');
}
putchar ('\n');
}
int main (void) {
int intNum = 123456789;
float fltNum = intNum;
double dblNum = intNum;
printf ("%d %f %f\n",intNum, fltNum, dblNum);
dumpBits ("Integer", (unsigned char *)(&intNum), sizeof (int));
dumpBits ("Float", (unsigned char *)(&fltNum), sizeof (float));
dumpBits ("Double", (unsigned char *)(&dblNum), sizeof (double));
return 0;
}
我的系统输出如下:
123456789 123456792.000000 123456789.000000
integer:
00000111 01011011 11001101 00010101
float:
01001100 11101011 01111001 10100011
double:
01000001 10011101 01101111 00110100 01010100 00000000 00000000 00000000
我们将逐一查看这些内容。首先是整数,即2的幂次方:
00000111 01011011 11001101 00010101
||| | || || || || | | | +-> 1
||| | || || || || | | +---> 4
||| | || || || || | +-----> 16
||| | || || || || +----------> 256
||| | || || || |+------------> 1024
||| | || || || +-------------> 2048
||| | || || |+----------------> 16384
||| | || || +-----------------> 32768
||| | || |+-------------------> 65536
||| | || +--------------------> 131072
||| | |+----------------------> 524288
||| | +-----------------------> 1048576
||| +-------------------------> 4194304
||+----------------------------> 16777216
|+-----------------------------> 33554432
+------------------------------> 67108864
==========
123456789
现在让我们来看一下单精度浮点数。请注意尾数的位模式与整数的近乎完美匹配:
(译者注:这段内容主要涉及计算机内部数据类型的二进制表示方式)
mantissa: 11 01011011 11001101 00011 (spaced out).
integer: 00000111 01011011 11001101 00010101 (untouched).
在尾数的左侧有一个隐含的 1 位,并且它也在另一端进行了舍入,这就是精度损失的来源(即从程序输出中值从 123456789
改变为 123456792
的情况)。
计算出数值:
s eeeeeeee mmmmmmmmmmmmmmmmmmmmmmm 1/n
0 10011001 11010110111100110100011
|| | || |||| || | |+- 8388608
|| | || |||| || | +
|| | || |||| || +
|| | || |||| |+
|| | || |||| +
|| | || |||+
|| | || ||+
|| | || |+
|| | || +
|| | |+
|| | +
|| +
|+
+
符号为正,指数为128+16+8+1 = 153 - 127 bias = 26
,因此乘数为2的26次方,即67108864
。
尾数为1
(隐式基数)加上(如上所述){1/2, 1/4, 1/16, 1/64, 1/128, 1/512, 1/1024, 1/2048, 1/4096, 1/32768, 1/65536, 1/262144, 1/4194304, 1/8388608}
。将它们全部相加,得到1.83964955806732177734375
。
将其乘以2的26次幂的乘数,得到123456792
,与程序输出相同。
双精度掩码输出为:
s eeeeeeeeeee mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
0 10000011001 1101011011110011010001010100000000000000000000000000
我不会计算那个数的值 :-) 但是,我会显示幂位数作为整数格式旁边的二进制表示以展示其通用比特表示:
我不想浪费时间计算那个数的值,但是我会显示它在整数格式下的尾数以展示它的通用位表示:
mantissa: 11 01011011 11001101 00010101 000...000 (spaced out).
integer: 00000111 01011011 11001101 00010101 (untouched).
左边的隐式位和右边宽广得多的位可见有共性,因此在这种情况下没有精度损失。
转换float和double之间的数值也相对容易理解。
首先您需要检查特殊值,例如NaN和无穷大。这些由特殊的指数/尾数组合表示,最好提前检测并生成相应的格式。
然后,在从double转换为float的情况下,由于指数中的位数较少,因此可用范围较小。如果您的double超出了float的范围,则需要处理它。
假设它适合,接下来您需要:
- 重新设置指数的基数(两种类型的偏差不同)。
- 复制尽可能多的尾数位(必要时舍入)。
- 使用零位填充目标尾数的其余部分(如果有)。