前言
先说一下写这篇文章的起因,源于昨天一个朋友问我问题:
代码对应如下:
#include <stdio.h>
#include <math.h>
int main () {
int a, b;
scanf("%d%d", &a, &b);
printf("%d", pow(a, b));
return 0;
}
相信知道pow
返回值为double
的同学,应该都可以大概可以猜到原因就和double
为64位,在进行整型格式化输出时,导致高32位截断,低32位全为0有关,下面是pow
函数的函数声明,可以看到pow
返回类型为double
:
_CRTIMP double __cdecl pow (double, double);
接下来就来具体探究原因。
IEEE 浮点表示
C语言的浮点数表示采取了IEEE
浮点标准,这里不详细展开,具体信息可以查看IEEE 754浮点数标准详解,或者查看《深入理解计算机系统》这本书(以下简称为CSAPP
)的 2.4 浮点数中的部分,该书的 pdf 文件链接也在文末给出,在正式讲解之前先说明本文并不会详细讲明浮点数的底层表示标准,将尽可能用简单且容易理解的方式向你介绍,并让你了解造成这一结果的原因,以及以后编程中需要注意的东西。
IEEE
浮点标准使用 V = (-1)s * M * 2E 的形式来表示一个数(相信学过科学计数法的对这一表示都感到熟悉,下列解释为了便于理解有删减,但不影响整体的认识,想深入学习的还是建议查看CSAPP
):
符号(sign) s
是决定该数(V)为正(s = 0
)还是为负(s = 1
);尾数(significand) M
是一个二进制小数,一般都是1 ~ 2
之间,就像在十进制中,这个对应的范围是1 ~ 10
之间一样;阶码(exponent) E
可以简单理解为我们在十进制科学计数法中的指数(可为负)。
此外浮点数的位表示被划分成了三部分:
- 使用最高的一位代表符号位(s);
- k 位的阶码
exp
(由上述的 E 计算得出); - n 位的小数段
frac
(由上述的 M 计算得出)。
在 C 语言中,我们以 32 位的float
和 64 位的double
为例:
- 对于
float
而言,上述的s, exp, frac
分别取值为 1,8,23; - 对于
double
而言,上述的s, exp, frac
分别取值为 1,11,52。
对应下图:
在讲解如何讲一个浮点数转换为上图表示之前,我们先了解一下,如何计算浮点数的二进制表示。
小数的二进制
我们先看一个十进制的例子:对于小数0.29
,即对应 2 * 10-1 + 9 * 10-2,而二进制的表示也是同理:对于二进制小数 0.11
,即对应1 * 2-1 + 1 * 2-2(0.5 + 0.25 = 0.75)。那么我们又如何从一个十进制小数得到对应的二进制小数呢,我们以一个能够精确转换的0.625
为例,由于整数部分为 0,所以对应二进制小数的整数部分也为 0,对于小数部分的计算使用一下规则:从第一个小数位开始,每个小数位等于当前十进制小数位乘 2 后的整数位(为 0 或者 1 ),然后当前数变为乘 2 后的小数位,直到当前数为 0 为止,这样解释可能有点绕,下面就通过0.625
的例子来详细展示:
当前十进制小数 | 乘 2 后的结果 | 整数部分 | 小数部分 | 得到的二进制小数位 |
---|---|---|---|---|
0.625 | 1.25 | 1 | 0.25 | 1 |
0.25 | 0.5 | 0 | 0.5 | 0 |
0.5 | 1.0 | 1 | 0 | 1 |
因此,通过查看最后一列,我们就得到了0.625
对应的二进制小数0.101
,下面我就开始步入正题啦!
由十进制数得到 IEEE 表示数
在这里我们仅以十进制转化为double
为例(为了方便等下讲解的开篇的pow
函数问题),float
转换也同理,我们就先以pow(3, 0)
的结果27
为例,对应二进制数为11011
,先转换为IEEE
浮点标准的形式:1.1011 * 24,然后我们来得到s, exp, frac
的对应结果:
- 27 为正,故最高位 s 为0;
exp
为 1023 + E = 1023 + 4 = 1027(在float
中这个1023需要改变为127),对于 1027,其对应的 11 位二进制为1000 0000 011
(在float
中exp
只有 8 位);frac
取 M 的小数段并在末尾补0,补够 52 位(在float
中为 23 位),因此frac
对应的二进制结果是1011
后面再加 48 个 0。
因此,我们最终得到27
对应的二进制数为0100 0000 0011 1011 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
。
我们可以在Java
中通过以下代码来验证:
public static void main(String[] args) {
// output: 100000000111011000000000000000000000000000000000000000000000000
// 注意:打印结果是从第一个不为0的位开始打印的,上述结果对应我们自己计算得到的后63位
System.out.println(Long.toBinaryString(Double.doubleToLongBits(27)));
}
由于我们运行的环境中int
为32位,而double
为 64 位,因此格式化输出时会对原数进行截断处理,只取低 32 位,也就得到了0。
当然你可能还是不太相信格式输出的时候做了这些东西,那么我们就来自己模拟一个数来进行验证,假如我们想让一个不是 29 的double
数字(我们要反推计算出的数字),在格式化输出后打印了 29,那么我们就来开始模拟:
- 首先 29 对应的二进制数为
11101
,由于最终要打印 29,我们就需要将低 32 位设置为11101
并在前面加上27个0,而为了方便,加上double
的frac
部分共有 52 位,因此我们就将frac
部分设置为11101
前面加上 47 个 0; - 然后由于 29 为正数,因此最高位 s 对应为0,然后我们再计算稍微麻烦些的
exp
部分; - 由于
frac
部分是由 M 的小数部分得到的,而我们这次小数部分已经占用到了frac
的最后部分(29
在frac
的末尾),因此可知 E 对应为 52,所以exp
为 1023 + E = 1023 + 52 = 1075,对应的 11 位二进制位为1000 0110 011
。 - 最终我们得到了
0100 0011 0011 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1101
这个数。
我们可以在Java
中通过以下代码得到其对应的浮点数:
public static void main(String[] args) {
// output: 4.503599627370525E15
System.out.println(
Double.longBitsToDouble(0b0100001100110000000000000000000000000000000000000000000000011101L)
);
}
然后我们可以在 C 代码中来验证这个浮点数如果使用整型格式化输出是不是会得到 29:
#include <stdio.h>
int main () {
printf("%d", 4.503599627370525E15);
return 0;
}
通过运行可以发现,我们的确得到了 29:
总结
本文简单介绍了IEEE
浮点标准的一些知识,当然深入了解还是需要多查阅资料,比如推荐的CSAPP
这本书,除此之外,也可以看到,在日常的编码中,我们还是需要非常注意一些类型转换的,尤其是缩窄变换,我们需要特别注意需不需要进行强制类型转换,例如开篇的代码我们在pow
函数前加上int
的强制类型转换也可以正常得到 27(但是还是需要特别注意某些情况下可能无法得到预期的结果,毕竟int
只有32位,而double
有64位):
#include <stdio.h>
#include <math.h>
int main () {
int a, b;
scanf("%d%d", &a, &b);
printf("%d", (int)pow(a, b));
return 0;
}
希望本文能够对你有所帮助,激起你看CSAPP
这本书的欲望。
参考资料
- 《深入理解计算机系统》
资料链接
链接:https://pan.baidu.com/s/19REaaTdx3SALq9vZBlmjrA
提取码:e5sl
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/5388.html