【数据在内存中的存储】

一、前言

我们人类记忆数值的方式与计算机存储数值的方式有什么区别?
这篇文章介绍整数、浮点数在内存中存储的方式与原理。

二、整数在内存中的存储

1.原码、反码、补码

对于整形来说:数据存放内存中其实存放的是补码

什么是补码?

整数的2进制表⽰⽅法有三种,即原码、反码和补码
有符号整数的三种表⽰⽅法均有符号位和数值位两部分
二进制序列中,最⾼位的1位是被当做符号
位,剩余的都是数值位。
符号位都是⽤0表⽰“正”,⽤1表⽰“负”
数值位则是正常的二进制计算

正整数的原、反、补码都相同。
负整数的三种表示方法各不相同

  • 原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
  • 反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
  • 补码:反码+1就得到补码。

以1为例
原码 反码 补码0000000000000000000000000000001

以-1为例
原码10000000000000000000000000000001
反码11111111111111111111111111111110
补码11111111111111111111111111111111

反码得到原码也是可以使⽤:取反,+1的操作;
也可以反着来-1,取反的操作。
用图来总结
在这里插入图片描述

2.有符号signed无符号unsigned

signed就是可+可-;unsigned就是只有+
signed与unsigned范围不同
以char为例
signed char:-128-127
unsigned char(就是char):0-255
(当然不同类型short,int的最大最小值由于字节数不同当然不同)

为什么会这样?

在这里插入图片描述对应signed char第一位是符号位,由127进位后不是数值上的增加而是符号的改变(不过10000000为-128倒是特殊规定)
在这里插入图片描述
unsigned char没有符号位,由127进位后就是数值上的增加

那么,如果数值超过了我的signed char和unsigned char的最大最小范围会怎么样呢?
答案是——————循环
以signed char为例
在这里插入图片描述

以下死循环代码就是基于上述原理

#include <stdio.h>

unsigned char i = 0;

//unsigned char 的取值范围是0~255
//到了255再加1就变成了0所以i<=255永远成立

int main()
{
    for (i = 0; i <= 255; i++)//死循环
    {
        printf("hello world\n");
    }

    return 0;
}
#include <stdio.h>
#include <windows.h>

int main()
{
    unsigned int i;// 表示的数都是>=0
    for (i = 9; i >= 0; i--)//变成“-1”时会变成一个非常大的数所以i>=0一直成立
    {
        printf("%u\n", i);
        Sleep(1000);//包含在windows里
    }
    return 0;
}

三、大小端字节序和字节序判断

1. 引入:一个疑惑

在这里插入图片描述
为什么这个数字在字节中是“倒着”存储的

2.什么是大小端

在这里插入图片描述

3.为什么有大小端

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着⼀个字节,⼀个字节为8bit 位,但是在C语⾔中除了8 bit 的 char 之外,还有16 bit 的 short 型,32 bit 的 long 型(要看
具体的编译器),另外,对于位数⼤于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度⼤于⼀个字节,那么必然存在着⼀个如何将多个字节安排的问题。因此就导致了⼤端存储模式和⼩端存
储模式。

理论上来说字节的存储可以不是顺序逆序的,而是任意顺序,只要取出来的时候能还原即可,比如
在这里插入图片描述

  • 左边VS右边:在阅读理解方便上,相比于右边的随意,左边的大小端明白更加清晰有逻辑。
  • 大端VS小端而对于大小端,并无明显的好坏之别
    我们常⽤的 X86 结构是⼩端模式,而KEIL C51 则为⼤端模式。很多的ARM,DSP都为⼩端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

相应地,我们可以是实现一个小功能

//设计一个小程序来判断当前机器的字节序
#include <stdio.h>
int check_sys()
{
	int n = 1;

	if (*(char*)&n == 1)
		return 1;//小端
	else
		return 0;//大端
}

int main()
{
	int ret = check_sys();
	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");

	//0x00 00 00 01

	//01 00 00 00
	//00 00 00 01
	return 0;
}

4.回顾:问题的解决

在这里插入图片描述
因为用的是x86结构是小端模式,因此11 22 33 44 存在内存中将低位字节存到低地址处 为44 33 22 11

四、浮点数在内存中的存储

1.引入:一道思考题

先给出一段代码。用整数存储的方式思考一下答案是多少?

#include <stdio.h>

int main()
{
    int n = 9;
    float* pFloat = (float*)&n;//int*
    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 9.0 9 9.0?在这里插入图片描述
但实际上的结果是……在这里插入图片描述
不必气馁,这恰恰说明了浮点数与整数存储方式的不同
要理解这个结果,⼀定要搞懂浮点数在计算机内部的表⽰⽅法

2.浮点数的存储-特殊的表示方式

根据国际标准IEEE(电⽓和电⼦⼯程协会) 754,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式
在这里插入图片描述
//举个例子来说
十进制的5.0等价于
二进制的101.0等价于
科学计数法表示则是1.01×2^2
相当于的这里的S=0,M=1.01,E=2//
不妨带入V的表达式看一看,是不是与科学计数法差不多的呢?
到这里,我们就可以做到十进制浮点数与上述表示形式的转化了

3.浮点数存储的内存分配与指数分析

如同整数4个4个bit的分配,浮点数也有其内存块分配的方式

对于32位的浮点数(float),最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/175862aa410449ccb0b78d5dad009df0.png
对于64位的浮点数(double),最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M
在这里插入图片描述

这里对于S E M分别做进一步研究。
这里放一组前面的案例进行比对:
(-1)^0 * 1.01 * 2^2
这里的S=0,M=1.01,E=2

  • S
    在这里插入图片描述

对于S 只有0/1之分 分别对应符号的+与-,很好理解。
知道了5.0,那么如果是十进制的-5.0呢?
写成⼆进制是 -101.0 ,相当于 -1.01×2^2
可以得到S=1,M=1.01,E=2

  • E
    在这里插入图片描述
    ⾸先,E为⼀个⽆符号整数(unsigned int)这意味着,如果E为8位,它的取值范围为0到255;如果E为11位,它的取值范围为0到2047。但是,我们知道,科学计数法中的E是可能出现负数的,比如我将0.1写成1.0×2^(-1)。所以IEEE 754规定,存⼊内存时E的真实值必须再加上⼀个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。
    (PS:为什么是127和1023?不必深究,对于非极端情况可以保证不为负数)
    指数E从内存中取出可以再分成三种情况

1.E不全为0或不全为1
这种情况最普通,比如
在这里插入图片描述
取出表示E的01111110 十进制为126减去127
得到真实值为-1
——————————————————————————
2.E全为0
在这里插入图片描述
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第⼀位的1,⽽是还
原为0.xxxxxx的⼩数。这样做是为了表⽰±0,以及接近于0的很⼩的数字。
——————————————————————————
3.E全为1
在这里插入图片描述
这时,如果有效数字M全为0,表⽰±⽆穷⼤(正负取决于符号位s);

  • M
    在这里插入图片描述
    我们规定(与科学计数法一致), 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中 xxxxxx 表⽰小数部分。
    比如0.1要写成1.0×2^(-1)
    IEEE 754 规定,在计算机内部保存M时,默认这个数的第⼀位总是1,因此可以被舍去,只保存后⾯的xxxxxx部分。⽐如保存1.01的时候,只保存01,等到读取的时候,再把第⼀位的1加上去。这样做的⽬的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第⼀位的1舍去以后,等于可以保存24位有效数字。

4.回顾:思考题的解决

#include <stdio.h>
//整数和浮点数在内存中的存储是不一样的~
int main()
{
    int n = 9;
    //00000000 00000000 00000000 00001001
    float* pFloat = (float*)&n;//int*
    //站在pFloat的角度0 00000000 00000000000000000001001
    //当内存中的E全为0的时候,真实值为1-127->-126,取出后不再加上第一位的1
    //(-1)^0 0.00000000000000000001001*2^-126相当于除了2的126次方约等于0
    printf("n的值为:%d\n", n);
    printf("*pFloat的值为:%f\n", *pFloat);

    *pFloat = 9.0;
    //9.0
    //1001.0
    //1.001*2^3
    //(-1)^0*1.001*2^3 S=0 M=1.001 E=3
    //0 10000010(3+127) 00100000000000000000000
    printf("num的值为:%d\n", n);
    //1091567616
    printf("*pFloat的值为:%f\n", *pFloat);
    //9.0
    return 0;
}

五、结语

数据在内存中怎么存储自然有一定的逻辑与道理,但更根本的是,它是被创造者制定出来的——有些合理有些不合理,有些方便有些麻烦不必深究,能够读懂能够使用即可。在这一块的学习上,往往接受比过度的质疑有用。
希望能有些帮助~~

  • 30
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值