当前位置: 首页 > news >正文

进制数知识(2)—— 浮点数在内存中的存储 和 易混淆的二进制知识总结

目录

1. 浮点数在内存中的存储

1.1 浮点数的大V表示法

1.2 浮点数的存储格式

1.3 浮点数的存入规则

1.4 浮点数的读取规则

1.5 补充:移码与掩码

1.6 题目解析

2. 易错的二进制知识

2.0 符号位到底会不会参与运算?

2.0.1 存储前的编码变化运算

2.0.2 存储后的数值算术运算

2.1 整数都以补码进行存储和运算 & 整型提升的2种情况

2.1.1 存储前的整型提升 与 补码

2.1.2 运算时的整型提升 与 补码(补码的运算)

2.3 unsigned对数据的本质影响

2.3.1 unsigned控制读取方式(打印方式),不控制数据的存储

2.3.2 unsigned控制运算时的整型提升

2.3.3 易错:用%u打印char型数据,不代表该数据被unsigned修饰

2.4 图示总结


1. 浮点数在内存中的存储

常⻅的浮点数:3.14159、1E10等,浮点数家族包括: float、double、long double 类型。 浮点数表⽰的范围在 float.h 中定义

1.1 浮点数的大V表示法

根据国际标准IEEE(电⽓和电⼦⼯程协会)754,任意⼀个⼆进制浮点数V可以表示成下⾯的形式:

                                                V = (-1)^{S} * M * 2^{E}

  • (−1)^S 表示符号位。当S=0,V为正数;当S=1,V为负数。 
  • M 表示有效数字,M大于等于1,小于2。(1 <= M < 2)
  • 2^E 表示指数位

其实这个公式就是二进制的科学计数法,这与十进制的科学计数法类似( (-1)^S * M * 10^E )

举例来说:

(1)十进制的5.0,写成⼆进制是:101.0 ,相当于 1.01×2^2

那么,按照大V表示法的格式,可以得出S=0M=1.01E=2

(2)⼗进制的-5.0,写成⼆进制是 -101.0 ,相当于 -1.01×2^2

那么,S=1,M=1.01,E=2

(3)十进制的0.25,写成二进制是 0.01,相当于 1× 2^(-2)

那么,S=0,M=1.0,E= -2

1.2 浮点数的存储格式

IEEE 754规定,对于32位的浮点数(float)

(1)最高的1位存储符号位S

(2)接着的8位存储指数位E

(3)剩下的23位存储尾数位M

IEEE 754规定,对于64位的浮点数(double)

(1)最高的1位存储符号位S

(2)接着的11位存储指数位E

(3)剩下的52位存储尾数位M

long double类型通常占用更多的内存空间,一般是10到12个字节(80到96位),但在某些系统上可能达到16个字节(128位)。这里不多做解释。

1.3 浮点数的存入规则

IEEE 754 对于有效数字M和指数E,还有⼀些特别规定。

M的存入规则:

  • 前⾯说过1≤M<2 。也就是说,M可以写成 1.xxxxxx 的形式,其中 xxxxxx 表示小数部分。
  • IEEE 754 规定,在计算机内部保存M时,默认这个数的第⼀位总是1,因此可以被舍去,只保存后面的 xxxxxx 部分

⽐如保存1.01的时候,只保存01,等到读取的时候,再把第⼀位的1加上去。

这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第⼀位的1舍去以后,等于可以保存24位有效数字。

E的存入规则:

  • 首先,E为⼀个无符号整数(unsigned int)

这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是科学计数法中的E是可以出现负数的。

  • 所以IEEE 754规定,存⼊内存时E的真实值必须再加上一个中间数(偏移量)。
  • 对于8位的E,这个中间数是127;(2的8次方是256,255 / 2 == 127)
  • 对于11位的E,这个中间数是1023。(2的11次方是2048,2047 / 2 == 1023)

⽐如,2^10的E等于10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

1.4 浮点数的读取规则

由于指数E有特殊情况,M的读取也跟着不一样:(主要分为三种情况)

1. E的存入值不全为0或不全为1(一般情况)

  • 指数E的存入值减去127(或1023),得到真实值
  • M的读取:得到真实值后,再将小数部分(尾数位)前加上第⼀位的1,变回1.xxxxxx 的形式。

⽐如:

十进制数0.5 的⼆进制形式为0.1,大V表示法为1.0 * 2^(-1)

其指数位E为-1+127(中间值)=126,存入为01111110

而尾数位M是1.0,去掉整数部分为0,补⻬0到23位 00000000000000000000000,则其⼆进制表示形式为:

0 01111110 00000000000000000000000 


2. E的存入值全为0

如果是2^(-127)的话,这个数太小了,无限接近0。由于这样的数字精度不太够,IEEE 754规定:

  • M的读取:尾数位不再加上第一位的1,⽽是当作为 0.xxxxxx 的小数来约分处理
  • 规定指数E等于 -126(或者-1022)即为真实值

该情况下的3种意义:这样做是为了表示±0,以及接近于0的数字

  1. +0:符号位为0,8个(或11个)指数位为全为0,23个(或52个)尾数位全为0
  2. -0:符号位为1,8个(或11个)指数位为全为0,23个(或52个)尾数位全为0
  3. 接近0的数字:8个(或11个)指数位为全为0,尾数位不全为0。

解析:

你可以理解成:有效数字从1.xxxxxx 变成了 0.1xxxxx 的形式,既然有效数字向右退位了,那么指数部分就要+1补位,所以E的真实值是1-127(或者1-1023)。


3. E的存入值全为1

如果是2^(128)的话,这个数太大了。这样的数字精度也不太够,IEEE 754规定:

  • M的读取:此时尾数位也不进行添1操作
  • 此时真实值E无效

该情况下也有三种意义:

  1. 正无穷(+∞或+inf):符号位为0,指数位全为1,尾数位全为0
  2. 负无穷(-∞或-inf):符号位为1,指数位全为1,尾数位全为0
  3. 不存在的数字(NaN,Not a Number):指数位全为1,尾数位存在1

1.5 补充:移码与掩码

还有几点我想要补充一下:

补充1:

  • 浮点数指数位的存储和运算,使用的不是原码、反码和补码,而是移码(“偏移量”或“偏移二进制编码”)

补充2:

  • 移码的运算规则:用二进制存储偏移后的E,用十进制来计算真实值的E。
  • 指数位虽然是无符号整型,但由于移码运算的特殊性(二进制存储,十进制计算),所以指数位不会发生数据截断

举个例子:

假如在float型中,E的真实值是-2,存入的过程并不是

  • 1111 1110 (-2的补码) + 0111 1111 (127的补码) 得到1 0111 1101,再截断多出的1位,变成0111 1101 (125的补码)

而是这样:

  • 存入时的10进制计算:-2+127=125
  • 以2进制存入:111 1101 (-2的移码)(这个就是数学上的二进制数字,并不是原码,反码或补码)
  • 取出时的10进制计算:先读取:111 1101 (2进制数字) == 125 (10进制数字);再计算:125 -127 = -2

补充3:

(1)尾数位M的存入:

尾数位采取掩码的方式存储。在计算机科学中,掩码通常是一个二进制序列,用来选择或隐藏特定的数据位。在浮点数的尾数位中,中隐藏了有效数字的1 。

(2)尾数位的大小:

还没补回1的尾数位序列,从左向右,位数依次减少最高位是2的-1次方

补充4:

浮点数的计算器 与 整数的计算器是不同的

(浮点数运算器被设计出来专门处理带有小数点的数值,采用不同的运算方式,这也是移码和掩码存在的意义 以及 移码运算性质不同的原因)

1.6 题目解析

判断下面这段代码会输出什么:

int main()
{int n = 9;float *pFloat = (float *)&n;printf("n的值为:%d\n",n);printf("*pFloat的值为:%f\n",*pFloat);*pFloat = 9.0;printf("num的值为:%d\n",n);printf("*pFloat的值为:%f\n",*pFloat);return 0;
}

代码结果:

这种情况出现的本质是,存储的方式与读取的方式不匹配。 

代码的上半部分中,整数9存入了整型变量n中,它的二进制编码是:

00000000 00000000 00000000 00001001(9的补码)

  • %d是以整数的方式读取内存(以补码的方式读取),读取的结果就是9
  • %f 是以浮点数的方式读取内存(以移码+偏码的方式读取),由于指数位全是0,且尾数位太小精度不够(默认显示6位小数),所以显示的是0.000000

代码的下半部分中,通过指针把浮点数9.0存入n的内存空间中,其二进制编码是:

【大概的样子】0 01111110 111001100110011001000000

( 0.9无法用二进制完全表示,约等于1.111 * 2^(-1) )

  •  %d当做整数去读取,这里最高的二进制位已经是2^30了,所以最终结果是一个很大的整数。 
  • %f 就正常读取一个浮点数,所以结果是9.000000。                  

2. 易错的二进制知识

2.0 符号位到底会不会参与运算?

我们知道,为了表示区分正负数,规定了数据类型的最高位二进制位为符号位。又由于计算机只有加法器,没有减法器,我们创造了补码。

原码的符号位和补码的符号位是一样的,那么符号位其实会不会参与运算呢?

这得分两种情况讨论:

2.0.1 存储前的编码变化运算

由原码得到补码的过程是:原码符号位不变,数值位按位取反得到反码,再对反码+1得到补码。

由补码得到原码的过程是:补码符号位不变,数值位按位取反得到补码的反码,再对该反码+1得到原码。

原码、反码、补码 相互转换,这些的过程就是编码变化运算。

我们注意到:由原码得到补码时,符号位并不会发生变化而且该运算发生在数据存储到内存空间之前。所以编码变化运算中的符号位并不会真实参与运算

计算机执行该运算的硬件是逻辑单元(ALU)。当需要将一个数的原码转换为补码时,计算机会检查原码的最高位(符号位),如果符号位为0(表示正数),则原码与补码相同;若符号位为1(表示负数),则需要将除符号位外的其他位取反(即0变为1,1变为0),然后整体加1。

2.0.2 存储后的数值算术运算

在数据保存在内存空间后(或暂存到内存后),此后的一系列算术运算,符号位会真实参与到算术运算当中


比如,我们用char计算2-1的结果:a = 2 - 1。

第一步:存储数据

2和-1的数据会暂存到加法器的内存中,由于没有减法器,我们采用的是补码的加法。

2的char大小的型补码是00000010;-1的char型大小的补码是11111111。

第二步:存储后的整型提升

由于char型数据太小,计算机会自动将他整型提升成int型大小的数据,按符号位提升。(紫色是提升后的字节,红色是char型数据的符号位)

整型提升后2的补码:00000000 00000000 00000000 00000010

整型提升后-1的补码:11111111 11111111 11111111 11111111

第三步:算术运算

此时才正式进行算术运算,两个补码提升后的结果:(黄色是进位后的下一个字节)

1 00000000 00000000 00000000 00000001

由于右边第2个二进制位1+1等于2要进位,导致后面的所有二进制位都进位了,所以多出了第33位二进制位。

第四步:数据截断

因为a是char型数据,装不下5个字节大小的数据,所以数据截断只剩下低位字节,即:00000001


从第3步可以看到,符号位也真实参与了算术运算,上下0+1等于了1,因为前面的数字进1,所以符号位最终的结果是“1+1等于0”。

人们常说:数值运算时,符号位不计算,只计算数值位就行了。其实这么说也不算错,由于补码算术运算的特殊性,确实造就了这句话的现实意义。(误区的来源)

但这样理解无疑是片面的,符号位也会真实参与到算术运算当中。

2.1 整数都以补码进行存储和运算 & 整型提升的2种情况

2.1.1 存储前的整型提升 与 补码

补码的存储:

对于较小整型的存储(或初始化),会先用较大的数据类型,以原码的形式表示出该十进制数字的二进制形式。然后把该较大型数据原码转换成补码。再对该二进制补码序列进行数据截断。

存储前的整型提升 的特性:

  1. 在​​​​​​​创建字节数较小的变量时,系统默认会先开辟4个字节或8个的空间,即存储前的整型提升
  2. 在默认内存空间中,符号位是该空间的最高二进制位。x86环境下,符号位是第32位;x64环境下,符号位是第64位
  3. 数据截断后会产生新的符号位
  4. 此时的整型提升不会被unsigned影响:数据是负数,最高位就是1;数据是正数,最高位就是0

比如,我们要用char存储-10:char a = -10;

第一步:用int型空间原码表示出该数字的二进制形式

-10的二进制原码表示:100000000 00000000 00000000 00001010 (红色的是符号位)

第二步:通过编码变化运算,转换成补码

转换为补码后的结果:11111111 11111111 11111111 11110110 (红色的是符号位)

第三步:数据截断,存入char型空间中

截断和存入的结果:11110110(新的符号位


2.1.2 运算时的整型提升 与 补码(补码的运算)

补码的运算:

较小的整型会先对补码进行整型提升,再对提升后的结果进行运算。(提升后的每一个二进制位都会参与运算

合适大小的整型可以直接对补码进行算术运算。

运算时的整型提升 的特性:

  1. 在字节数较小的数据运算时,会先进行整形提升,变成较大的数据。
  2. 会根据符号位进行补位提升。正数补0,负数补1
  3. 此时的整型提升会被unsigned影响

例子可以参考2.0.2的示例。

2.3 unsigned对数据的本质影响

2.3.1 unsigned控制读取方式(打印方式),不控制数据的存储

(1)对一个unsigned类型的变量赋值一个负数,不会因为unsigned修饰就让数据存储的最高位为0,仍然是正常地得储存。

(2)但以%u读取时会把符号位也当做数值位读取

代码演示:

int main()
{unsigned int a = -1;printf("以有符号数的形式读取:%d\n", a);printf("以无符号数的形式读取:%u\n", a);
}

-1用二进制存储是:

11111111 11111111 11111111 11111111 

以%d来读取,那就是-1;

以%u来读取,结果是2^32-1,即:4294967295。

2.3.2 unsigned控制运算时的整型提升

前面提到过,运算时的整型提升会被unsigned影响,具体是什么呢?

(1) 对于较小的unsigned整型,在运算时(存储后的数据),整型提升不再看最高位是0还是1,都统一用0来补位

例如:

int main()		
{unsigned char a = -1;printf("%u\n", a);return 0;
}

-1的存储仍然遵循 “原码表示二进制int型数字 ---> 转换为补码 ---> 数据截断” 的顺序。所以变量a中,-1的存储是11111111。

当以%u (unsigned int)的形式打印时,a的数据会先进行整型提升。而且a被unsigned修饰,整型提升是用0补位,变成:

00000000 00000000 00000000 11111111

所以结果是255。


2.3.3 易错:用%u打印char型数据,不代表该数据被unsigned修饰

我们用一段代码来演示:

int main()		
{char a = 128;printf("%u\n", a);char b = -128;printf("%u\n", b);return 0;
}

过程解析:

第一步:原码表示

128的原码表示:00000000 00000000 00000000 10000000

-128的原码表示:10000000 00000000 00000000 10000000 

第二步:补码转换

128的补码不变:00000000 00000000 00000000 10000000

-128的补码:11111111 11111111 11111111 10000000 

第三步:数据截断

128 和 -128都只剩下:10000000

第四步:打印前的整型提升

%u是unsigned int型,由于变量a和b都是char型,较小的整型就要进行整形提升

且它们都是char型,而不是unsigned char型,所以符号位仍然存在

有符号位时,按符号位来补位,它们都变成:

11111111 11111111 11111111 10000000

(如果是该数据是unsigned型的,那么这里补的就是0,而不是1了)

第五步:打印

由于以%u的形式输出,打印的时候把最高位当作数值位读取,所以结果就是这么大的数字。


2.4 图示总结

小数据类型的存储:

小数据类型的运算和输出:


本期分享完毕,感谢大家的支持~Thanks♪(・ω・)ノ


http://www.mrgr.cn/news/36360.html

相关文章:

  • 深圳前海壹方汇的免费停车点探寻
  • Java查找算法——(四)分块查找(完整详解,附有代码+案例)
  • 【mac开发入坑指南】分屏mac程序坞移动到另外一个屏幕
  • mysql学习教程,从入门到精通,SQL FULL JOIN 语句(25)
  • alpine安装docker踩坑记
  • 链表入门(LeetCode题目)
  • Claude 的上下文检索功能提升了 RAG 准确率,这会是人工智能革命?
  • C++深入学习string类成员函数(1):默认与迭代
  • yolov8训练数据集——labelme的json文件转txt文件
  • Keyence——PLC__Mitsubishi_PLC__Read_Write_Ascii
  • 遗忘的数学(拉格朗日乘子法、牛顿法)
  • 【Vision Transformer】辅助理解笔记
  • C++进阶——二叉搜索树
  • kibana开启访问登录认证
  • 如何在 Vue 3 项目中使用 Vuex 进行状态管理?
  • 开放原子开源基金会网站上的开源项目EasyBaaS存在内存泄露缺陷
  • 安卓简易权限调用
  • 文献阅读——基于拉格朗日乘子的电力系统安全域边界通用搜索方法
  • 制作一个能对话能跳舞的otto机器人
  • HashMap和Hashtabe的区别