有关计算机浮点数的思考。

计算机的浮点数只能近似表示一些非常精确的数。


由一个题目说起

有一道 C 语言的练习题,是这样的

c 复制代码
#include <stdio.h>

void main(void){

  int num=9; 

  float* pFloat=&num; 

  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=&num; 

  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
  1. 第二行,为什么是 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,所以答案就明了了。

  1. 第三行,为什么是 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。

相关推荐
林开落L30 分钟前
前缀和算法习题篇(上)
c++·算法·leetcode
Prejudices43 分钟前
C++如何调用Python脚本
开发语言·c++·python
单音GG1 小时前
推荐一个基于协程的C++(lua)游戏服务器
服务器·c++·游戏·lua
qing_0406031 小时前
C++——多态
开发语言·c++·多态
孙同学_1 小时前
【C++】—掌握STL vector 类:“Vector简介:动态数组的高效应用”
开发语言·c++
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js