浮点数在内存中的表示
1. 浮点数的二进制格式.
浮点数的二进制格式如下, 分为 3 个部分, 即 sign (符号位), exponent (指数位), 以及 significand (有效数位). 如下图所示:
single-precision floating point(单精度浮点数), 4 字节, 共 32 位:
31 23 22 0
+-----------------------------------------------------+
| s | exp(biased) | significand |
+-----------------------------------------------------+
double-precision floating point(双精度浮点数), 8 字节, 共 64 位:
63 52 51 0
+--------------------------------------------------------------------------------+
| s | exp(biased) | significand |
+--------------------------------------------------------------------------------+
最高位为符号位, 单精度浮点数的指数位 8 位, 有效数位 23 位; 双精度浮点数的指数位 11 位, 有效数位 52 位.
二进制浮点数采用类似十进制小数的科学计数法来表示(例如 3140.0 可以表示为 3.14 * 10 ^ 3), 即将任意浮点数都表示为 1.xxx~xxx * 2 ^ exponent 这种形式. 'x' 代表 0 或者 1, 毫无疑问 exponent 部分也只能是二进制形式的. 由于 1.xxx~xxx 中的 1 是固定的, 因此没有必要将其在 significand 中表示出来, 所以 significand 中只包含了 xxx~xxx 部分, 没有包含那个固定的 1, 即这个 1 是隐式存在的(或被称为 J-bit). 所以浮点数的表示式子也可以写作
J-bit.significand * 2 ^ exponent.
还要注意, 在图示中写的是 exp(biased), 而浮点数表示式子中写的是 exponent, 因为实际存储在内存中的指数 exp(biased) 是经过偏置(biase)后的 exponent.
在 x86 / amd64 中, 还有一种 double extended-precision floating point(扩展双精度浮点数), 10 字节, 共 80 位. 但是其 J-bit 是显式的(该位必须为 1, 否则为 unsupported 编码):
79 64 63 62 0
+---------------------------------------------------------------------------------------------+
| s | exp(biased) | J | significand |
+---------------------------------------------------------------------------------------------+
这是因为 x86 构架的浮点协处理器 x87 FPU 硬件上使用扩展双精度类型(就是说这个浮点协处理器的寄存器都是 10 个字节 80 位的). 如果编译器使用该协处理器来处理浮点数的话, 则编译器要负责将所有浮点数转换为扩展双精度, 调用 x87 FPU 指令进行浮点处理, 完事后再转换回来(略扯淡, 但是事实). 当然编译器也可以不使用 x87 FPU 用软件模拟浮点处理, 不然你以为 GMP (GNU Multiple-Precision, GNU 大数库, 号称地球第一快) 库是用来干嘛的, gcc 干嘛要依赖它.
2. 将十进制小数转换为二进制表示
先转换整数部分, 再转换小数部分(废话). 比如 3.14, 转换整数部分得到 11, 接下来转换小数部分(乘 2 取整):
0.14 * 2 = 0.28, 0
0.28 * 2 = 0.56, 0
0.56 * 2 = 1.12, 1
0.12 * 2 = 0.24, 0
0.24 * 2 = 0.48, 0
...
所以十进制 3.14 转换为二进制表示就是 11 . 00100...
这种转换手工算吃力不讨好, 干嘛不随便写个短程序解决它(当然只是为了随便看看, 不用写得很高大上):
1 #include <stdio.h> 2 3 void foo(int n) 4 { 5 int q, r; 6 7 if (n <= 0) 8 return; 9 10 r = n % 2; 11 q = n / 2; 12 13 foo(q); 14 15 printf("%c", r + '0'); 16 } 17 18 #define EPSILON 0.0000001 19 20 int main(void) 21 { 22 double d; 23 int inte_part; 24 double frac_part; 25 26 printf("input d: "); 27 scanf("%lf", &d); 28 29 inte_part = d; 30 31 foo(inte_part); 32 printf(" . "); 33 34 frac_part = d - inte_part; 35 while (!(-EPSILON < frac_part && frac_part < EPSILON)) 36 { 37 inte_part = frac_part * 2; 38 frac_part = frac_part * 2 - inte_part; 39 40 printf("%d", inte_part); 41 } 42 43 putchar('\n'); 44 45 return 0; 46 }
代码如上. 编译这个程序并运行, 例如输入 3.14 (注意不要输入负数. 输入负数的话, 代码中可以判断一下, 比如 if (d < 0) d = -d; 并先输出一个负号即可. 我忘了这档子事了), 我们得到其二进制表示为:
11 . 001000111101011100001010001111010111000010100011111...
嗯, 程序中有设定 EPSILON, 所以这个转换可能没完, 不过够用就行.
3. 查看 C 语言 float 类型变量的内存表示
这个不难. 不过仍然有一点需要注意. 类型决定对类型实例之上的操作是否合法, 对于 C 语言的 float 类型定义的实例对象来说, 不能直接对它进行位操作. 因此需要通过指针将该浮点对象一个字节一个字节地取出来再做位操作. 代码如下:
1 #include <stdio.h> 2 3 int main(void) 4 { 5 float f; 6 char *p = NULL; 7 char bits[32]; 8 9 printf("f = : "); 10 scanf("%f", &f); 11 12 p = (char *)&f; 13 14 int i, j; 15 for (i = 0; i < 4; i++) 16 { 17 for (j = 0; j < 8; j++) 18 { 19 (*p & (0x1 << j)) 20 ? (bits[i * 8 + j] = '1') 21 : (bits[i * 8 + j] = '0'); 22 } 23 p++; 24 } 25 26 // 从 0 --> 31 位顺序打印. 27 for (i = 0; i < 32; i++) 28 { 29 putchar(bits[i]); 30 } 31 putchar('\n'); 32 33 // 分别打印符号位, 指数, 有效数位. 34 printf("1 bit, sign: %c\n", bits[31]); 35 printf("8 bit, exp(biased): "); 36 for (i = 30; i > 22; i--) 37 putchar(bits[i]); 38 putchar('\n'); 39 printf("23 bit, significand: "); 40 for (i = 22; i >= 0; i--) 41 putchar(bits[i]); 42 putchar('\n'); 43 44 return 0; 45 }
运行, 输入 3.14, 得到如下运行结果:
f = : 3.14
11000011101011110001001000000010 // 注意, 这是按 0 --> 31 顺序打印的. 符号位是第 31 位, 0
1 bit, sign: 0
8 bit, exp(biased): 10000000
23 bit, significand: 10010001111010111000011
这个运行结果首先是将浮点数的内存表示从 0 --> 31 由低位到高位按位打印(由于在 x86 平台上, 代码是按照小端格式写的). 下面的 sign, exp, significand 依次为内存中的符号, 指数部分和有效数位部分.
对比一下将十进制 3.14 转换为二进制表示的结果: 11 . 001000111101011100001010001111010111000010100011111... 这个用浮点表示式子应该写为:
1. 1001 0001 1110 1011 1000 0101... * 2 ^ 1
嗯, 符号位(第 31 位)是 0, 因为是正数所以符号位是 0, 这个没有问题.
有效数位呢? 1001 0001 1110 1011 1000 011 这个也没有问题(J-bit没有在sigificand中), 最后的 011 是由 0101 舍入 (round) 的, 因为单精度浮点有效数位只有 23 位啊, 摊手.
指数位呢? 不应该是 00000001 (1) 吗, 为什么是 10000000 (128)?
之前说过, 内存中的指数是要经过偏置的. 对于单精度浮点数来说, 这个偏置值是 127, 即(2 ^ 8 / 2 - 1). 所以指数偏置后 1 + 127 == 128. 为什么要经过偏置呢? 因为 IEEE 754 考虑, 如果一个数指数大, 那么它肯定比指数小的浮点数要大(J-bit固定为 1). 在比较指数大小这种事情上, 只用正数(负数 -127 加上偏置值 127 后为 0). 而当指数位为全 1(-128) 的情况下用来表示无穷大. 所以就有了这个偏置值了. 同理双精度浮点数的偏置值是 2 ^ 11 / 2 - 1 == 1023.
关于 IEEE 754, 还有浮点舍入的问题有空了再写罢.
以上就是一个 float 类型实例在内存中的表示了. 对于 double 类型, 方法是一样的, 不过在将十进制转换为二进制的表示上要通过 C 代码得到准确的精度输出的话就那个呵呵呵了. 嗯, 还有比如给函数传递浮点参数时编译器怎么处理之类的问题, 困了...~.