同样是占 32 个坑位,为什么 float 比 int 的范围更大?
这里先说明一下,假设是在 32 位的机器上,int 占用 32 个 bit 位。而 float 使用的是 IEEE 754 标准的单精度浮点数格式,也是占用 32 个 bit 位。
这时候 float 和 int 都是占用 32 位,占用同样的空间,但 float 的表数范围更大的。那我们为啥还要 int 呢?为啥不用占用相同位大小,还能表示更大数范围的 float?现在我们来一探究竟!
int 对 32 个坑位是如何使用的?
int 类型的使用方法,大多学过计算机的,应该都是非常清楚的。二进制存储即可。
例 1:请写出 165(10 进制)使用 32 位 int 型存储在计算机中的形式。
首先求出 165(10 进制)的二进制数。
十进制整数怎么转换为二进制数呢?方法是“除二取余数的逆”。
| 除数 | 被除数 | 余数 |
|---|---|---|
| 2 | 165 | 1 |
| 2 | 82 | 0 |
| 2 | 41 | 1 |
| 2 | 20 | 0 |
| 2 | 10 | 0 |
| 2 | 5 | 1 |
| 2 | 2 | 0 |
| 2 | 1 | 1 |
| 2 | 0 | 0 |
| 0 |
余数从下到上,连起来就是 10100101。
所以 165 的二进制数为 10100101。
165 在 32 位 int 型中是这样存储的(中间的 0 省略了):
| 1 | 2 | 3 | ... | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 |
非常好理解。把 10 进制转换 2 进制,直接存进去就 ok,前面空位补 0。
例 2:请写出 -165(10 进制)使用 32 位 int 型存储在计算机中的形式。
-165(10 进制)是一个负数,按照惯例 int 型首位为符号位。0 表示在正数,1 表示负数。
在计算机中,正数和负数存储的都是补码,而不是原码。只不过正数的补码和原码保持一样。 反码是在原码的基础上,除了符号位,其它都取反。 补码则是在反码的基础上加 1。
165(10 进制)的二进制为 10100101,所以 -165(10 进制)的
原码为:1000 0000 1010 0101
反码为:1111 1111 0101 1010
补码为:1111 1111 0101 1011
最终,-165 在 32 位 int 型中是这样存储的(中间的 1 省略了):
| 1 | 2 | 3 | ... | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 |
比正数复杂了一点,但是还是可以很容易算出来的。
那么为什么负数要用补码呢?原码不好吗?
因为用补码计算更方便。比如我们要计算 165 + (-165),可以直接将它们的补码进行相加,得到的结果就是正确的值 0。 至于为什么直接将它们的补码进行相加就能得到一个正确计算的结果,现在还不得而知。只要记住这个结论就可以了!
float 对 32 个坑位是如何使用的?
同样也是占用 32 个坑,float 型的范围比 int 就大很多,而且还能表示小数,那么它到底是如何利用这 32 个坑的呢?
例 3:请写出 165.25(10 进制)使用 float 型存储在计算机中的形式。
10 进制的 165.25 转换为二进制,应该怎么表示呢?
可以先分开转换。整数 165 转换为二进制表示就是 10100101;而 0.25 转换为二进制使用的算法规则和整数并不一样,只需要知道最后经过计算表示为 0.01。
所以 10 进制的 165.25 转换为二进制,最终为 1010 0101.01
那么上面的二进制小数(1010 0101.01)是以什么形式存到 32 个坑里的呢?
在填坑之前,我们先要用科学计数法规范二进制小数的表示形式。
1010 0101.01 表示为
用科学计数法规范二进制小数后,二进制小数的整数部分只有一位,且一定为 1。(因为一个二进制小数,不管整数部分是否为 0,小数部分都一定含有 1,用科学计数法表示后,整数部分肯定只有一位且一定为 1。如果这个二进制小数是个负数,那它的补码的第一为也肯定是 1)
因为整数部分恒为 1,所以我们只需要存储符号、指数、小数部分。
IEEE754 标准把 float 型的 32 个坑做了如下划分:
| S(符号位,占 1 位) | E(阶码(指数部分),用二进制表示,占 8 位) | M(小数部分,占 23 位) |
|---|---|---|
| 0 | 1111 1110 | 1111 1111 1111 1111 1111 111 |
我们要存储的二进制小数为
那么符号位 S 就是 0,表示正数。
小数部分 M 就是 0100 1010 1000 0000 0000 000,从左到右填充,没有的空位填 0。
然后阶码 E(指数)本来实际是 7,但是根据标准要求为了阶码不会有负数,所以 E 实际存储时会加上 127 等于 e + 127(实际的阶码 + 127)。
所以实际存储的阶码值 E = (7 + 127)(十进制) = (134)(十进制) = (1000 0110)(二进制)。
最后把这三个数 S、M、E 填进坑位里,165.25(10 进制)使用 float 型存储在计算机中的形式就长下面这样子啦:
| S | E | M |
|---|---|---|
| 0 | 1000 0110 | 0100 1010 1000 0000 0000 000 |
弄清楚了上面的问题后,再来回答下面这三个问题
- 1、浮点数的表示范围有多大?
- 2、为什么要用指数加上 127,才是阶码 E,而不是直接用指数存进去?
- 3、这个过程可以看出 float 有效位是尾数 M 加符号位 S 也就是 24 位,阶码 E 只是我们规范科学计数法记录指数的。但 int 有效位是 32 位,float 实际有效位比 int 少,那么在相互转换的过程中会出现什么问题呢?
1、浮点数的表示范围有多大?
无穷大
标准规定,当阶码 E = 1111 1111 时,浮点数表示为无穷大,此时小数部分必须全部为 0,不能有其他值,否则就认为无效数字。
| S | E | M |
|---|---|---|
| 0 | 1111 1111 | 0000 0000 0000 0000 0000 000 |
当 E = 1111 1111 时,指数为 128(255 - 127,因为最开始人为加了 127)
可上表展示的数并不是
无穷小
同理,标准还规定了负无穷大存储的形式长下面这样:
| S | E | M |
|---|---|---|
| 1 | 1111 1111 | 0000 0000 0000 0000 0000 000 |
最大正整数、最小负整数
那么除了无穷大这个特殊的、人为定义的情况,float 型能表示的最大的正整数是多少?最小的负整数是多少?当 E= 1111 1111 时,是 IEEE754 定义的特殊值即为无穷大,那么除此之外的最大值就是:E = 1111 1110,M 也取最大值,即得到如下结果:
| S | E | M |
|---|---|---|
| 0 | 1111 1110 | 1111 1111 1111 1111 1111 111 |
此时阶码 E 为 254,那么指数 e 为 127(256 - 1 - 1 - 127),那么最大正整数为:
10 - 0.0000 0000 0000 0000 0000 001
又可以写成
所以 float 能够表示的最大正整数就是 3.4028 2346 638 E 38。
同理,float 能够表示的最小负整数就是: -3.4028 2346 638 E 38。
2、为什么要用指数加上 127,才是阶码 E,而不是直接用指数存进去?
我们希望存储到机器里的阶码永远都是正值,因为我们不希望再浪费一个坑去保存阶码的正负号,于是乎,干脆把指数加上 127。因为指数能取到的最小值就是 -127,这样就可以保证阶码 E 永远都是正数啦,我们就不用再考虑指数正负号的问题了。
3、这个过程可以看出 float 有效位是尾数 M 加符号位 S 也就是 24 位,阶码 E 只是我们规范科学计数法记录指数的。但 int 有效位是 32 位,float 实际有效位比 int 少,那么在相互转换的过程中会出现什么问题呢?
通过问题 1 知道,float 型的表示范围是比 int 大很多的,但有效位确实只有 24 位。既然 float 范围大,那么所有的 int 型都是可以转换为 float 型的,这是不会产生溢出报错的。但因为 int 型有效位是 32 位,是比 float 型的 24 位大的,是有可能发生舍入的,即当一个 int 型数字,转成 float 型后,可能就不再是原本数字了,损失了一定的精度。
例如 2 进制 int 型正数:0111 1111 1111 1111 1111 1111 1111 1111;
写成科学计数法为:
小数点后面有 30 个 1,但是我们知道 float 的小数部分 M 只有 23 个坑,所以存储的时候只会存储前面的 23 个 1,后面的 1 全部被舍弃了。
阶码 E 等于指数部分加上 127,为 30 + 127 = 157 = (1001 1101)(二进制)。
| S | E | M |
|---|---|---|
| 0 | 1001 1101 | 1111 1111 1111 1111 1111 111 |
可以发现,我们原 int 型中存储的数字如果转成科学计数法,小数本来有 30 个。但是用 float 保持后,小数点只能保留 23 个 1,而后面 7 个直接忽视了,这就是发生了舍入。
如果既要范围大,还要保留精度,那就上双精度浮点型 double,double 型的存储规则和 float 型是十分类似的。double 型有 64 个坑位,包括了 1 个符号位 S,11 个阶码位 E 和 52 个尾数位 M。所以 double 的有效位有 53 位,可以完整保留 int。
ok,同样是占 32 个坑,那凭啥你 float 就比 int 的范围更大?因为 float 型虽然范围大,但是精度不足!
float 通过小数乘以指数的形式存储数据,范围大但精度不足。
int 则是实打实的存储整数的每一位,范围小但够精确。