小数在内存中是如何存储的?为什么C语言中的浮点数不支持位移操作?
上节课我们讲了如何计算2的1024次方,于是就有同学提出想法,既然浮点数表示的范围足够大,干脆定义一个浮点数,左移1024位就能得到结果。
位移确实是一种效率很高的解决方法,只是对浮点数位移操作,大部分的编译器都是不允许的。
#include
int main()
{
float f = 10.25;
f = f << 1;
return 0;
}
编译结果:
root@Turbo:test# gcc test.c -o test
test.c: In function ‘main’:
test.c:7:8: error: invalid operands to binary << (have ‘float’ and ‘int’)
7 | f = f << 1;
| ^~
root@Turbo:test#
整数在内存中的存储很容易让人接受,比如:
int a = 2;
假设a占4个字节,就是32位,那么前30位都是0,后面两位是10,稍微懂点计算机基础就能看明白。
但是浮点数的存储完全不一样,同样是4个字节的float类型,我们来看下在内存中是如何排布的。
国际标准IEEE规定,任意一个二进制浮点数V可以表示成这种形式。
这个表达式里面包含三个重要的参数,SME。
前面这个部分表示符号位,S为0,表示-1的0次方,就是1,此时为正数。S为1,表示-1的1次方,就是-1,此时为负数。
M表示有效数字,范围大于等于1,小于2,这个我们待会举例子来看。
最后一个E表示指数,就是多少次方的意思。
对于32位的float类型,S占一位,M占23位,E占8位。
下面还要搞明白,如何把小数转换成二进制。
先来个简单的,5.25,其中整数部分5二进制就是101,小数部分0.25使用乘2取整。
0.25乘2等于0.5,整数0;
0.5乘2等于1.0,整数1。
当小数部分为0的时候,运算结束。于是5.25的二进制表示形式就是这样的。
101.01
再来个复杂的。
3.14,3的二进制是11,0.14的二进制就比较麻烦。
算了半天也没发现小数部分什么时候等于0。
而浮点数位数又是有限的,所以只能保存它的近似值。
当然,如果是double类型,位数更多,那保存的数据就会更加精确。
有了这两个基础,再来看下浮点数怎么存放到内存中。
假设浮点数10.25。
先把它转换成二进制,10的二进制就是1010,0.25的二进制是01,于是10.25的二进制是这样的。
1010.01
再把它转换成标准形式,就是这样的:
1.01001 * 2^3
等价于:
(-1)^0 * 1.01001 * 2^3
分成了三个部分,分别对号入座。
S肯定就是0,表示正数。
M理论上是1.01001,不过别人也早就发现了,不管什么样的小数,整数部分肯定都是1,所以留着也没啥意义,不如把它去掉,这样还能多出一位,表示更好的精度。
最后是E,显然就是3。
但是直接写3也不行,IEEE规定,E是一个无符号整数,就是它只能是正数,但是科学计数法中,E有可能是负数,所以他又规定,E的真实值需要再加上一个中间数,对于8位的E来说,中间数是127,最终E的结果是130,二进制就是这样的。
10000010
SME三个都有了,接下来就是对号入座,最终得到的二进制就是这样的。
我们来验证下对不对,把它转换成十六进制就是0x41240000。
定义浮点数10.25,把这四个字节的地址强转成整型指针,然后直接把四个字节内容输出,得到的也是41240000。
#include
int main()
{
float f = 10.25;
int *p = (int *)&f;
printf("%x
", *p);
return 0;
}
浮点数在内存中的存储比较复杂,没有整数那么直观。如果把它从内存里面读出来,也还要经过大量的运算才能还原成小数。
审核编辑:汤梓红