首页 > 系统相关 >进制数知识(2)—— 浮点数在内存中的存储 和 易混淆的二进制知识总结

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

时间:2024-09-25 22:19:54浏览次数:3  
标签:存储 运算 符号 浮点数 知识 补码 内存 整型

目录

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♪(・ω・)ノ

标签:存储,运算,符号,浮点数,知识,补码,内存,整型
From: https://blog.csdn.net/2301_80030290/article/details/142426650

相关文章

  • 【c++实战项目】从零实现一个高并发内存池
    主页:醋溜马桶圈-CSDN博客专栏:实战项目_醋溜马桶圈的博客-CSDN博客gitee:mnxcc(mnxcc)-Gitee.com目录1.项目介绍1.1 这个项目做的是什么?1.2这个项目的要求的知识储备和难度?2.什么是内存池2.1 池化技术2.2 内存池2.3 内存池主要解决的问题2.4malloc3.开胃......
  • Swift查看变量内存地址
    withUnsafePointer不说话,先放代码withUnsafeBufferPointer(to:a){pointinletaddress=UnsafeRawPointer(point)letaddressInt=Int(bitPattern:address)print("\(addressInt)")}验证copyonwrite值......
  • 00 内存分配 -- 重点
    要确认是进行赋值,还是找到其中,有赋值为:重新开辟内存空间python将:-5~256为常用的数字(如果在范围类使用同一内存空间,这叫:python小数据池)如果大于这个数值,会重新进行开僻内存空间字符串:如果A1=‘’alexA2=‘alex,A1/A2等于同一个字符串,理应不是同一个内存地址,需要重......
  • 01 内存地址 示例
    示例一:v1=[11,22,33]v2=[11,22,33]v1=666v2=666v1="asdf"v2="asdf"#以上数据都不是同一个内存地址#按理v1和v2应该是不同的内存地址。特殊:1.整型:-5~2562.字符串:"alex",'asfasdasdfasdfd_asdf'----"f_*"*......
  • 04 内存回顾 练习题
    """v1=[1,2,3]v2=[1,2,3]v1=[1,2,3]v2=v1v3=v1v1.append(999)#v2、V3指向V1内存地址,如果V1改变,V2/V3也会改变v1=[1,2,3]v2=v1v3=v1v1=[1,]#v2、V3指向V1内存地址,V1赋值,V2/V3指向V1内存地址,V1指向赋值内存地址:1v1=[1,2,3]v2=......
  • 05 字典内存分配
    data_list=[]foriinrange(10):data={}data['user']=idata_list.append(data)print(data_list)#每个字典都不一样字典,列表内存指向图data={}foriinrange(10):data['user']=iprint(data)内存占用图......
  • C/C++语言基础--C++类数据、静态与非静态、常成员、友员、成员变量与函数指针等相关知
    本专栏目的更新C/C++的基础语法,包括C++的一些新特性前言通过前面几节,我们介绍了C++的类与对象、构造与析构函数、拷贝等相关知识,这一篇将详细介绍了C++的成员变量相关的知识点与扩展C语言后面也会继续更新知识点,如内联汇编;本人现在正在写一个C语言的图书管理系统,1000多......
  • EfficientViT(2023CVPR):具有级联组注意力的内存高效视觉Transformer!
    EfficientViT:MemoryEfficientVisionTransformerwithCascadedGroupAttentionEfficientViT:具有级联组注意力的内存高效视觉Transformer万文长字,请耐心观看~论文地址:https://arxiv.org/abs/2305.07027代码地址:Cream/EfficientViTatmain·microsoft/Cream......
  • Elasticsearch知识整理(包含与mongoDb的区别)
    Elasticsearch概念整理Elasticsearch是位于ElasticStack核心的分布式搜索和分析引擎。Logstash和Beats有助于收集、聚合和丰富您的数据并将其存储在Elasticsearch中。Kibana使您能够以交互方式探索、可视化和分享对数据的见解,并管理和监控堆栈。Elasticsearch......
  • c语言中字符串输入的相关知识点
    (1)scanf只能接收非空格字符串遇到空格或者换行就算结束。代码如下:#include<stdio.h>#include<stdlib.h>intmain(){chararr[10];scanf("%s",&arr);printf("%s",arr);}(2)gets函数-能够接收空格,但是不能接收回车#include<stdio.h>#include&l......