计算机的浮点数只能近似表示一些非常精确的数。
由一个题目说起
有一道 C 语言的练习题,是这样的
c
#include <stdio.h>
void main(void){
int num=9;
float* pFloat=#
printf("num的值为:%d\n",num);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat=9.0;
printf("num的值为:%d\n",num);
printf("*pFloat的值为:%f\n",*pFloat);
}
运行结果如下:
bash
coral@xx:~/workspace/csapp$ ./float
num的值为:9
*pFloat的值为:0.000000
num的值为:1091567616
*pFloat的值为:9.000000
-
第一行,直接输出 num 的值,这个毫无疑问是对的。
-
第二行,输出为什么为 0?
float 和 int 类型的变量存储空间都是 4 个字节。令一个 float 类型的指针指向 num 的地址,然后通过
*float
的方式访问,会将 num 地址内存储的内容以 4 字节浮点数解析。 -
第三行,输出为什么是一个类似乱码的数值?
将原来 num 变量的内容通过 float 类型的指针采取
*float
的方式赋值为 9.0,然而输出的时候按照 int 的方式输出,所以解析错误,出现乱码。 -
第四行,这个就没有疑问了
通过 float 类型指针,然后通过 float 类型指针访问,所以解析方式是相同的,所以答案正如所料。
总之,这个题就是需要考虑 int 类型和 float 类型的存储格式。如果按照不同的方式解析肯定会出现错误。
一句话
内存只是一个字节数组而已,不管是 float、int 还是其他类型的变量,不过是内存中需要的大小或者是存储格式不同而已,无其他区别。
int 的存储格式
int 是 4 字节类型,也就是 32 位。int 的存储格式就是 32 位补码。这个不必多言,很好理解。这篇文章重点写浮点数的存储格式。
float 的存储格式
float 的存储格式正如下图,在这里我们只考虑简单的单精度浮点数和双精度浮点数:
科学计数法
因为浮点数的需求是要表示小数、特大数和非常接近零的数。当然由于浮点数精度总是有限的,所以有一些数值只能近似表示,而不能完全相等。
下面再来回想一下科学计数法,当然这我们很早之前就学过,比如下面这个数:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 5.21 × 1 0 1314 5.21\times 10^{1314} </math>5.21×101314
如果我们不使用科学计数法的话就需要用 1313 位数字来表示,这样就太麻烦了。所以就采用了科学计数法来表示,用于节省 "空间"。
"二进制" 科学计数法
与此目的相同,计算机为了表示一些数,就采用了 "二进制" 版本的科学计数法,比如数值 7.0,就可以用以下 "二进制" 科学计数法表示:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 1.11 × 2 2 1.11 \times 2^2 </math>1.11×22
它是怎么来的呢?因为 7.0 的二进制表示为 111,将小数点左移两位,相应的就需要乘上 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 2^2 </math>22来使其相等。
浮点数的存储格式的想法就来自于此,因为这样能够大大减少存储空间,另外一个好处就是能够在误差允许的范围内表示非常大的数( <math xmlns="http://www.w3.org/1998/Math/MathML"> ± ∞ \pm\infty </math>±∞)或者非常接近于 0 的数 ( <math xmlns="http://www.w3.org/1998/Math/MathML"> f → 0 f\to0 </math>f→0)。
浮点数一般用如下方式表示:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> ( − 1 ) S × M × 2 E (-1)^S\times M\times2^E </math>(−1)S×M×2E
- S 用于表示浮点数的符号
- M 用于表示有效数字,
- E 用于表示指数
所以计算机只需要存储这三部分即可。
单精度浮点数的存储格式
与想象的方式还有点不同,除了一些优化外,还需要遵守一些约定。
下面的表格便是 32 位浮点数大端法表示的存储格式:
sign(符号) | exp(指数) | frac(有效数字小数部分) |
---|---|---|
1 位 | 8 位 | 23 位 |
- 32 位浮点数的存储格式,大端法表示就是符号 + 指数 + 有效数字小数部分
- 因为有效数字必须满足 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 ≤ f r a c < 2 1\le frac<2 </math>1≤frac<2,所以二进制的小数点前的一位总是 1,所以省略不写
- 因为指数不仅需要表示正数次幂,也需要能表示负数次幂,可能是字节对齐的缘故,所以指数部分不能采用补码形式表示,而是采用 IEEE 754 规定采用找中间数的方法,中间数总是 <math xmlns="http://www.w3.org/1998/Math/MathML"> ⌊ 2 E M a x − 1 2 ⌋ \lfloor\frac{2^{EMax}-1}{2}\rfloor </math>⌊22EMax−1⌋,比如 8 位指数,中间数就是 <math xmlns="http://www.w3.org/1998/Math/MathML"> ⌊ 2 8 − 1 2 ⌋ = 127 \lfloor\frac{2^8-1}{2}\rfloor=127 </math>⌊228−1⌋=127。在表示的时候需要将真实值+中间数。
- E 的规定
- E 不全为 0 并且不全为 1。浮点数的值就是 E 减去中间数 127 得到指数真实值,然后有效数字小数部分 M 前面加上 1
- E 全为 0。浮点数的指数为 1 - 中间数(32 位浮点数为 1-127),有效数字前不再+1,这样就为 0.xxxx 的小数,而且可以表示非常接近于 0 的数。
- E 全为 1。如果有效数字全为 0,则表示 ;否则就表示这不是一个数 NaN。
双精度浮点数的存储格式
解析方法与单精度浮点数相同,存储格式类似。
下面的表格便是 64 位浮点数大端法表示的存储格式:
sign(符号) | exp(指数) | frac(有效数字小数部分) |
---|---|---|
1 位 | 11 位 | 52 位 |
例题题解
c
#include <stdio.h>
void main(void){
int num=9;
float* pFloat=#
printf("num的值为:%d\n",num);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat=9.0;
printf("num的值为:%d\n",num);
printf("*pFloat的值为:%f\n",*pFloat);
}
bash
coral@xx:~/workspace/csapp$ ./float
num的值为:9
*pFloat的值为:0.000000
num的值为:1091567616
*pFloat的值为:9.000000
- 第二行,为什么是 0.00000?
执行 printf("*pFloat的值为:%f\n",*pFloat);
语句时,变量存储空间内是存储的 int 类型的 9,二进制表示为 0000-0000-0000-0000-0000-0000-0000-1001,但是输出使用 float 指针类型索引,所以按照此类型解析的话,二进制解析为 0-00000000-00000000000000000001001,所以指数 E 为全 0。浮点数真值为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> V = ( − 1 ) 0 × 0.00000000000000000001001 × 2 − 126 = 1.001 × 2 − 146 V=(-1)^0×0.00000000000000000001001×2^{-126}=1.001×2^{-146} </math>V=(−1)0×0.00000000000000000001001×2−126=1.001×2−146
数值几乎为 0,所以答案就明了了。
- 第三行,为什么是 1091567616?
这个同理了,只不过是反向思考。
首先用 float 指针将数值设置为 9.0。9 的二进制表示为 1001,用" 二进制 "科学计数法表示为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1.001 × 2 3 1.001\times2^3 </math>1.001×23,所以 S 为 0,E 为 3+127=130(二进制为 10000010),有效数为 001,所以变量存储的内容为 0-10000010-00100000000000000000000,然后将这个值用 int 类型解析,0100-0001-0001-0000-0000-0000-0000-0000,结果为 1091567616。