修炼内功:研究数据在内存中的储存

✨✨小新课堂开课了,欢迎欢迎~✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:http://t.csdnimg.cn/Oytke

小新的主页:编程版小新-CSDN博客

上级给小新发布了任务,小新要带领大家去击败大BOSS,我们来一起做任务吧。

只有弄清楚数据在内存中的储存,我们才能获胜,大家要一起加油哦~

大BOSS现在扔给了我们一道题,解答对就可以通关。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

int main()
{
	int n = 9;
	float* cp = (float*)&n;
	printf("n的值:%d\n", n);
	printf("*cp的值:%f\n", *cp);

	*cp = 9.0;
	printf("n的值:%d\n", n);
	printf("*cp的值:%f\n", *cp);
	return 0;
}

并且大BOSS告诉了我们,答案绝对不是9 ,9.0 ,9 ,9.0;

这也能说明整数和浮点数在内存中的储存方式有所不同,具体有什么不同呢?

你知道正确答案是什么嘛,不知道的话,我们就一起来解答吧,现在我们就要修炼内功了。

章法一.整数在内存中的储存

大家应该知道整数在内存中是以二进制的形式储存的,储存方式有三种形式,即原码,反码,补码。正数的原码,反码,补码相同,负数就不同了。

三种表示方式中均有符号位数值位,其中最高位被当成是符号位,最高位是0代表该数为正数,最高位是1代表该数为负数。正数的原码,反码,补码相同。让我们看看负数有什么不同吧。

负整数的三种表⽰⽅法各不相同:

原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。

反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。

补码:反码+1就得到补码。

而内存中储存的就是二进制的补码,在打印的时候,就要把其对应的原码给算出来,以原码对应的数值打印。

这里做一个小小的扩展,大家知道内存为什么以补码的形式储存吗?

在计算机系统中,数值⼀律⽤补码来表⽰和存储。 原因在于,使⽤补码,可以将符号位和数值域统⼀处理; 同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

章法二.大小端字节序和字节序的判断

在对这部分的知识进行学习之前,我们先来观察一个现象吧。

大家有没有想过,为什么整数在内存中的储存这里怎么有种倒着存的感觉呀,小新是有这个疑问的,那就和我接着探索吧

1.什么是大小端

其实超过一个字节的数据在内存中储存的时候就有了储存顺序的问题,按照不同的储存顺序,我们分为大端字节序存储和小端字节序存储,具体概念如下:

大端字节序存储:把一个数据的低位字节的内容存储到高位字节处,把高位字节的内容存储到低                                 位节处。

小端字节序存储:把一个数据的低位字节的内容存储到低位字节处,把高位字节的内容存储到高                                 位字节处。

那我们一起来分析一下上面的代码,看一看它的存储顺序是什么吧。

2.为什么要有大小端

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着⼀个字节,⼀个字节为8个 bit位,但是在C语⾔中除了8个bit位的 char 之外,还有16个bit位的 short 型,32个bit位的 long 型(要看 具体的编译器),另外,对于位数⼤于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度⼤ 于⼀个字节,那么必然存在着⼀个如何将多个字节安排的问题。因此就导致了⼤端存储模式和⼩端存 储模式。 例如:⼀个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为⾼字节, 0x22 为低字节。对于⼤端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在⾼地址中,即 0x0011 中。⼩端模式,刚好相反。我们常⽤的 X86 结构是⼩端模式,⽽ KEIL C51 则为⼤端模式。很多的ARM,DSP都为⼩端模式。有些ARM处理器还可以由硬件来选择是 ⼤端模式还是⼩端模式。

这里大家做一个简单的了解即可。

3.实践

下面我们举几个例子,相信大家就会明白了。这里的例子是要用到上面的知识的哦

例子1:

曾经百度出了一个面试题,要我们模拟实现判断大小端的问题

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int check_sys()
{
	int i = 1;//00 0000000000000000000000000000 01
	return *((char*)&i);//转化为char*,一次访问一个字节,达到我们的目的
	                  //这里解引用之后如果是按大端字节序储存就是00,按照小端字节序储存就是01
}


int main()
{
	int ret = check_sys();
	if (ret == 1)
	{
		printf("小端字节序\n");
	}
	else
	{
		printf("大端字节序\n");
	}
	return 0;

这里的环境是vs2022,内存的存储时按照小端字节序有存储的,如果你有其他的编译器,可以下去试一下。

例子2:

#include<stdio.h>
//下面都是以%d的形式打印的,%d是一次访问四个字节
int main()
{
	char a = -1;
	signed char b = -1;
	unsigned char c = -1;
	printf("a=%d,b=%d,c=%d", a, b, c);
	return 0;
}

如果想要做对上面的例题,就要了解下面的知识,我们来一起剖析这个题吧

在正式分析这个题目之前,和小新一起更加清晰的了解一下signed char 和 unsigned char 吧。 

接下来和小新一起看一下这一题的整体思路吧。

#include<stdio.h>
//下面都是以%d的形式打印的,%d是一次访问四个字节
//char是有符号char,还是无符号char是取决于编译器的
//在VS里指的是有符号char
int main()
{
	char a = -1;
	//10000000000000000000000000000001  -1的原码
	//11111111111111111111111111111110  -1的反码
	//11111111111111111111111111111111  -1的补码
	//char只能存8个bite位
	//11111111 a
	//11111111111111111111111111111111  类型提升 补码
	//10000000000000000000000000000000
	//10000000000000000000000000000001  原码
	//-1
	signed char b = -1;
	//同上
	unsigned char c = -1;
	//这里只是把-1先存进去,至于最后是什么样,我们这里补考虑
	//10000000000000000000000000000001  -1的原码
	//11111111111111111111111111111110  -1的反码
	//11111111111111111111111111111111  -1的补码
	//char只能存8个bite位
	//11111111 c  所以这里c还和上面一样是11111111
	//00000000000000000000000011111111  类型提升
	//%d认为内存中存放的是有符号数,最高位为0,代表是正数,原码,反码,补码相同
	//255
	printf("a=%d,b=%d,c=%d", a, b, c);//以%d打印这里会发生类型提升,并且%d认为内存中储存的是有符号数
	//                      -1  -1  225
	return 0;
}

例子3:

小新相信再给大家一个例子,大家就会熟练掌握这个做题方法啦

#include<stdio.h>
int main()
{
	char a = -128;
	//10000000000000000000000010000000 原码
	//11111111111111111111111101111111
	//11111111111111111111111110000000
	//10000000 a
	//11111111111111111111111110000000  类型提升
	//无符号数可以认为是正数,原码,反码,补码相同
	char b = 128;
	//00000000000000000000000010000000
	//10000000 b
	//11111111111111111111111110000000
	printf("%u\n", a);//%u认为内存中存的是无符号数
	printf("%u\n", b);
	return 0;
}

例子4:

这个例子又会打开你的新思路哦~,小新感觉又要长脑子了

#include<stdio.h>
int main()
{
	char a[1000] = { 0 };
	int i = 0;
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	printf("%d", strlen(a));//计算'\0'之前的元素个数,'\0'的ascii码值是0
}

答案为255,快看看是为什么吧

看一看在0之前是不是255个数呢~,这是以为因为char类型只能存放8个bite位的原因 

章法三:浮点数在内存中的储存 

了解了上面的知识,离我们打败怪兽就更近了一步,下面我们学习新的章法了,大家做好准备。

浮点数家族中有float,double,long double,他们的取值范围都定义在float.h中。

1.浮点数的储存

根据国际标准IEEE(电⽓和电⼦⼯程协会)754,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式:V   =  (−1) ^ S *M ∗ 2^ E  ,

•(−1) 表⽰符号位,当S=0,V为正数;当S=1,V为负数,

• M表⽰有效数字,M是⼤于等于1,⼩于2的(二进制是0,1序列)

• 2^E 表⽰指数位

举例来说: ⼗进制的5.0,写成⼆进制是 101.0 ,相当于 1.01×2^2(向左移动两位,即E=2) 。 那么,按照上⾯V的格式,可以得出S=0,M=1.01,E=2。 ⼗进制的-5.0,写成⼆进制是 -101.0 ,相当于 -1.01×2^2 。那么,S=1,M=1.01,E=2。 IEEE754规定:对于32位的浮点数,最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M;对于64位的浮点数,最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M。

文字不好理解的话,那我们就上图。

 

这下应该更直观了~,画个图真不容易呀,有没有人知道快一点的方法呀,简单吐槽一下,嘻嘻~

总结一下,其实浮点数的储存存的就是S,E,M的值。 

2.浮点数存的过程 

IEEE 754对有效数字M和指数E,还有⼀些特别规定。 前⾯说过, 1≤M,也就是说,M可以写成 1.xxxxxx 的形式,其中 xxxxxx 表示小数部分。 IEEE754规定,在计算机内部保存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;对于11位的E,这个中间数是1023(这里是为了避免负数的情况,加上中间数后,就恰好达到了这样的效果)。比如,2^10的E是 10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001

这里给大家举个例子吧

#include<stdio.h>
int main()
{
	float f = 5.5;
	//写成二进制
	//101.1  
	//1.011*2^2
	//(-1)^0*1.011*2^2
	//S=0
	//M=1.011
	//E=2
	//加上中间值127是129 即10000001
	//所以内存中的存储如下
	//0 10000001 01100000000000000000000 
	//S   E       M
	//如果要转化位16进制就是
	//0100 0000 1011 00000000000000000000
	//40 B0 00 00
	return 0;
}

我们看图片中的地址,前面大小端的知识有没有忘记呢,这里是不是小端存放呢。

这里大家在自己举例的时候,注意举得例子不要太过特殊,不然你就会发现,储存的值是不精确的,当然这个情况出现的概率是比较小的,小新给大家准备了一个例子,来看一下吧。

#include<stdio.h>
int main()
{
	float f = 0.4554;
	printf("%.10f\n", f);
	return 0;
}

3.浮点数取的过程 

指数E从内存中取出还可以再分成三种情况:

E不全为0或不全为1 这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第⼀位的1。

 比如:0.5的⼆进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其 阶码为-1+127(中间值)=126,表⽰为01111110,而尾数1.0去掉整数部分为0,补⻬0到23位 00000000000000000000000,则其⼆进制表⽰形式为:

0 01111110  00000000000000000000000

E全为0 这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第⼀位的1,而是还 原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。

解释一下吧,如果E=0,那是加上127或者1023之后为0的,那原本的E就是-127或者-1023,E代表的是指数,如果指数是-127或者-1023的话,1.xxxx*2^-127或者1.xxxx*2^-1023那将是一个非常小的数字。

E全为1 这时,如果有效数字M全为0,表示±无穷⼤(正负取决于符号位s)

如果是float类型的,8个bite位全存1,就是255,真实的E就是128,1.xxxx*2^128,那将是一个很大的数字

理论知识我们就学到这里,大家还记得我们修炼内功是干嘛的吗,大BOSS扔给我们的难题还没有解答,我们还没有取得胜利,大家现在已经取得真经,就让我们一起来破解难题,在这里提前预祝大家闯关成功喽~

#include<stdio.h>
int main()
{
	int n = 9;                  //9:   00000000000000000000000000001001
	float* cp = (float*)&n;     //站在整型的角度,以整型的形式存进去,又以整型的形式打印,打印的结果是正常的
	printf("n的值:%d\n", n);    //所以n的值就是9
	printf("*cp的值:%f\n", *cp);//但以整型的形式存进内存又站在float的角度上去打印,就会有不同的效果
	                            //0 00000000 00000000000000000001001
                               // S   E        M
	                           //浮点数取的过程不要忘记哦,我们发现E全为0,返回的时候E的真实值就是1-127=-126
	                           //有效数字不在加上第一位的1
	                           //(-1)^0*0.001*2^-126  这是一个非常小的数字,而float最多打印小数点后六位
	                           //结果就是0.000000


	*cp = 9.0;                  //9:   00000000000000000000000000001001
	printf("n的值:%d\n", n);    //站在浮点数的角度存进去,又以整型的形式打印出来
	printf("*cp的值:%f\n", *cp);//存进去的是 (-1)^0*1.001*2^130
	return 0;                   //0 10000010 00100000000000000000000
}                               //以整型的形式打印这是一个非常大的数 1091567616
                               //站在浮点数的角度存进去,有以%f打印出来
                               //取得过程,就是观察E,找到E的真实值
                               //(-1)^0*1.001*2^3
                               //cp=9.000000

全文将近6600字,希望大家能多多支持,点赞收藏哦,不然就找不到小新了,大家也可以在评论留言,告诉小新大家的想法,来一场和小新的双向奔赴吧~

下次在再和小新一起打怪吧,很有意思的,不信你就关注看看嘛

  • 15
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值