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

news2024/9/27 17:29:55

目录

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.coloradmin.cn/o/2165389.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Zynq从零开始】汇总导航

Welcome 大家好&#xff0c;欢迎来到瑾芳玉洁的博客&#xff01; &#x1f611;励志开源分享诗和代码&#xff0c;三餐却无汤&#xff0c;顿顿都被噎。 &#x1f62d;有幸结识那个值得被认真、被珍惜、被捧在手掌心的女孩&#xff0c;不出意外被敷衍、被唾弃、被埋在了垃圾堆。…

《Object-Centric Learning with Slot Attention》中文校对版

系列论文研读目录 例如&#xff1a;第一章 Python 机器学习入门之pandas的使用 文章目录 系列论文研读目录摘要引言2 方法2.1 插槽注意力模块2.2 对象发现2.3 集合预测 3 相关工作4 实验4.1 对象发现4.2 集合预测 5 结论更广泛的影响 摘要 学习复杂场景的以对象为中心的表示是…

erlang学习:Linux命令学习5

从本地上传文件 使用rz命令选择文件&#xff0c;将本地文件上传至linux服务器 rz与本地文件中的txt文本文件内容相同 将上传的文件按行分割 split -l 1 study.txt -d -a 1 study_-l:按行分隔&#xff0c;每1行对study.txt文件进行切割 -d:添加数字后缀 -a 1:以1位数数字做尾…

Java_Day03学习

Day03 构造方法目的 为类的属性赋值构造方法调用 //调用带参方法时&#xff0c;要先实例化类&#xff0c;创建对象再调用&#xff1b; eg&#xff1a;public class Max_and_Min {//方法:访问修饰符 返回类型 方法名(形式参数类型 形参&#xff0c;形参类型 形参) {}public v…

速刷DuckDB官网24小时-掌握核心功法

简介 DuckDB 是面向列的本地 OLAP 数据库&#xff0c;SQLite是面向行的本地 OLTP 数据库。duckdb是 数据分析师得力助手&#xff0c;可以很好的直接读取本地的各种结构化数据文件&#xff0c;速度显著快于主流的pandas等工具。 duckdb通过加载插件可以成为各种RDBMS数据库的中…

Linux突发网络故障常用排查的命令

测试环境 系统&#xff1a;Ubuntu 18硬件&#xff1a;单核2G ping 用于测试客户机和目标主机通信状况&#xff0c;是否畅通。以及测量通信的往返时间&#xff0c;判断网络质量的好坏。 它通过发送ICMP回显请求消息到目标主机&#xff0c;并等待返回的ICMP回显回复消息。 pin…

C#和数据库高级:继承与多态

文章目录 一、继承的基本使用继承的概念&#xff1a;继承的特点&#xff1a;为什么使用继承&#xff1f; 二、继承的关键字1、this关键字2、base关键字3、Protected关键字4、子类调用父类的构造函数的总结&#xff1a; 三、继承的特性继承的传递性&#xff1a;继承的单根性&…

C/C++逆向:循环语句逆向分析

在逆向分析中&#xff0c;循环语句通常会以特定的汇编模式或结构体现出来。常见的循环语句包括 for 循环、while 循环和 do-while 循环。由于不同的编译器会根据代码优化的级别生成不同的汇编代码&#xff0c;分析循环的模式也可能会有所不同。以下是三种常见循环语句的汇编分析…

【源码+文档+调试讲解】人事管理系统设计与实现Python

摘 要 人事管理系统的目的是让使用者可以更方便的将人、设备和场景更立体的连接在一起。能让用户以更科幻的方式使用产品&#xff0c;体验高科技时代带给人们的方便&#xff0c;同时也能让用户体会到与以往常规产品不同的体验风格。 与安卓&#xff0c;iOS相比较起来&#xff…

【数据结构】剖析二叉树(Binary Tree)

目录 &#x1f4af;引言 &#x1f4af;二叉树的定义与基本概念 &#xff08;一&#xff09;定义 &#xff08;二&#xff09;节点结构 &#xff08;三&#xff09;二叉树的形态 &#x1f4af;二叉树的遍历 &#xff08;一&#xff09;前序遍历&#xff08;Preorder Trav…

机器人控制器设计与编程基础实验高效版本-ESP32等单片机实验报告

只需要课程大纲或进度表wokwi 大模型工具&#xff0c;就可以完全掌握嵌入式系统基础实验的所有核心点。 LCD // Learn about the ESP32 WiFi simulation in // https://docs.wokwi.com/guides/esp32-wifi https://wokwi.com/projects/321525495180034642#include <WiFi.h>…

【AI学习】Lilian Weng:What are Diffusion Models?

读OpenAI 的 Lilian Weng博客《What are Diffusion Models?》 文章链接:https://lilianweng.github.io/posts/2021-07-11-diffusion-models/ 通过浏览器的在线翻译&#xff0c;直接截图了。翻译的有些问题&#xff0c;但是基本能大概看明白了。 我只是个人的记录&#xff0c;…

开发经验总结: 读写分离简单实现

背景 使用mysql的代理中间件&#xff0c;某些接口如果主从同步延迟大&#xff0c;容易出现逻辑问题。所以程序中没有直接使用这个中间件。 依赖程序逻辑&#xff0c;如果有一些接口可以走读库&#xff0c;需要一个可以显示指定读库的方式来连接读库&#xff0c;降低主库的压力…

降准降息一揽子措施点燃 A 股激情,4% 大涨之后趋势深度剖析

文章目录 牛回速归原因分析引爆点情绪和信心一根大阳线&#xff0c;千军万马来相见阴霾是否一扫而空还未可知 流动性和增量 潜在隐患等待经济复苏配套政策期待中美关系进展 短期内趋势分析空军短期内仍有余力如何看待第2日的回撤外围 趋势分析结论短期内可能仍有波折中长期会是…

【数学分析笔记】第3章第4节闭区间上的连续函数(1)

3. 函数极限与连续函数 3.4 闭区间上的连续函数 3.4.1 有界性定理 【定理3.4.1】 f ( x ) f(x) f(x)在闭区间 [ a , b ] [a,b] [a,b]上连续&#xff0c;则 f ( x ) f(x) f(x)在闭区间 [ a , b ] [a,b] [a,b]上有界。 【证】用反证法&#xff0c;假设 f ( x ) f(x) f(x)在 [ …

2-103 基于matlab的光电信号下血氧饱和度计算

基于matlab的光电信号下血氧饱和度计算&#xff0c;光转换成电信号时&#xff0c;由于动脉对光的吸收有变化而其他组织对光的吸收基本不变&#xff0c;得到的信号就可以分为直流DC信号和交流AC信号。提取AC信号&#xff0c;就能反应出血液流动的特点。这种技术叫做光电容积脉搏…

【Linux学习】2-1 Linux系统下运行C语言输出hello word

1.双击打开VMware软件&#xff0c;点击开启此虚拟机后&#xff0c;等待点击头像输入密码进入 2.“CtrlAltt”调出命令行终端&#xff0c;输入命令sudo apt-get install vim安装vim&#xff0c;输入命令sudo apt-get install gcc安装gcc 3.输入命令vi hello.c进入C语言编写环境&…

Linux —— Socket编程(一)

一、本篇重点 1. 认识IP地址、端口号、网络字节序等网络编程中的基本概念 2. 学习Socket api的基本用法 3. 能够实现一个简单的udp客户端/服务器 二、基本概念 1. 理解源IP地址和目的IP地址 简单的理解&#xff0c;IP地址是用于标识一台机器的&#xff0c;我们通过IP地址去…

使用 UWA Gears 测试小游戏性能

UWA Gears 是UWA最新发布的无SDK性能分析工具。针对移动平台&#xff0c;提供了实时监测和截帧分析功能&#xff0c;帮助您精准定位性能热点&#xff0c;提升应用的整体表现。 随着小游戏的规模和用户量持续增长&#xff0c;玩家对于小游戏的性能要求也越来越高。为了能够给玩…

力扣234 回文链表 Java版本

文章目录 题目描述代码 题目描述 给你一个单链表的头节点 head &#xff0c;请你判断该链表是否为 回文链表 。如果是&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,2,1] 输出&#xff1a;true 示例 2&…