我们在使用某一个变量时有没有想过这个变量在内存中是如何储存的呢?是我们输入一个十进制的值,内存中就直接储存这个十进制的值,还是别的内容呢?
我们首先说结论,那就是整数在内存中是以一个二进制补码的方式来存储的。
我们以整形int为例,一个整形是4个字节,一个字节是8个比特位,那么一个整形就是32个比特位。
整形在内存中就是以这32个比特位来储存的。对于正整数来说其实就是把一个整数化为二进制,然后前面补0补够32位就行。
我们就以整形4为例,它的二进制是 100,那我们只需要在前面补0就行
那可能会有人说这是正整数,那负数怎么表示,要是直接换成二进制的话那个负号该怎么表示,难道内存中还会储存负号?
内存中当然是不会储存负号的,但是也有办法来表示负数,对于一个有符号整形,规定这个整形的二进制序列的最高位是符号位。
一个整形不是32个比特位,最前面的就是他的符号位,符号位是0就是正数,为1就是负数。
我们刚刚说正整数在内存中的存储就是把十进制的数字换为二进制然后前面补0就行,但是对于负数就不是那么简单了。
这里就涉及3个概念,二进制的原码,反码和补码
原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就是补码。
负数在内存中存储的就是补码,我们以-2为例
其实也很简单,反码实际上对我们现在来说是没什么用的,我们只需要记住原码除符号位以为取反+1就是补码就行,同理其实补码取反+1其实就是原码。当然这些规则是对负数来说的,对于正数来说原码反码补码都相同。
到这里我们就可以下个结论了,整数在内存中是按照补码来储存的,无论是正整数还是负整数。在进行运算时也是用补码来算的。
可能会有人疑惑为什么会要储存补码,而不是直接储存原码,要搞这么麻烦。实际上这是为了统一运算,把减法转为加法,因为CPU中只有加法器,只能实现加法,对于减法就无能为力,所以引入补码就是对实施减法时,将其转换为加上一个负数来实现,两个数的相加实际上是补码相加,这样就可以来实现减法。
这里我是用的VS2022进行的操作,别的可能不同
可能有些同学可能会想亲眼看看某个数在内存中到底是怎么储存的,这也是有方法的,先按f10或f11调试起来,这两个键都是让程序向下运行一行,具体差别就要去专门研究研究,这里就不展开讲。
要注意的是要想看到某个变量的值,要先让程序运行过创建变量所在的那一行。
成功打开内存窗口后,我们可以是两个数字或者字母组成一对,这实际上是一个字节,但是列数太多了,我们点击右上角有个自动,然后调成4列,一个整形是四个字节,四列就正好是四个字节,一个整形。
但是我们刚刚学习的储存的是二进制序列,而二进制序列是一个个比特位啊,八个比特位是一个字节,所以要想判断是不是对的,还要将二进制序列转换为十六进制序列。
那么我们来学习一下二进制与十六进制的转换。
其实也很简单,就是四个二进制位转换为一个十六进制位,我们就以1来看
3.1大小端储存
假设我有一个变量a,值是0x11223344,0x表示十六进制,我这里直接把它的值设置为一个十六进制的值是方便接下来观察。
按照我们的最本能的想法,它在内存中应该是这样储存的 11 22 33 44,但是如果我们在内存窗口中看它是怎么储存的,会发现是这样的 44 33 22 11,如果我们观察过前面的地址编号,就会发现它的地址是从低到高的,也就是从左到右是从小到大的。所以这里实际也是从小到大排列的。
这就涉及到了大小端储存了,一个整形在内存中存储是有两种储存方式,大端存储和小端存储。
大端储存:低字节的数据储存在高地址的位置,高字节的数据储存在低地址位置。
小端储存:低字节的数据储存在低地址的位置,高字节的数据储存在高地址位置。
注意这个大小是对于低字节的数据来说的。
所以我们可以看到在我使用的VS中它是低字节的数据储存在低地址处,是小端储存。
不同的电脑可能这个储存方式会不同,可能是大端还是小端,至于为什么会有这个大小端储存,这个我也不知道,是跟计算机硬件相关的,我们了解概念以及如何判断就行。
3.2大小端储存的判断
知道了这个大小端的判读,那么我们该如何进行判断呢,有人可能会说直接调试看看就行,但是这是在IDE上,要是一个线上OJ题目呢,你没办法看到内存,这时候该这么去进行判断?
我们可以将问题准换为判断这个机器是大端储存,如果这个问题是真,那么就是大端储存,否则就是小端储存,那么我们要证明就只需要找出一个特殊的例子来证明就行,那1不就是最好的例证吗,1转转为十六进制的数字就是0x00000001,那如果是大端储存就是 00 00 00 01,如果是小端储存就是 01 00 00 00,那我们只需要找到第一个1的第一个字节不就行了,看看这个字节是0还是1就能判断了。
那么问题就变成了怎么找到第一个字节的值,这个也很简单,只需要用到一个char *类型的指针就行,char *类型的指针的访问权限就是访问一个字节,那我们只需要拿到某个变量的地址,将它强转为char *类型就行。
4.1一道练习
在开始讲之前,我们先来看一段代码
你们可以先想想结果是什么,这里我就放结果了
怎么样,跟你们想的结果是不是有点出入,没有关系,我们来了解一下浮点数在内存中的存储,随后再来看这个问题,那个时候就能理解了。
4.2浮点数的储存
举个例子:
十进制的7.0,写成二进制是 111.0,相当于 1.11 * 2^2。
那么,按照上面V的格式,可以得出 S = 0, M = 1.11, E = 2
十进制的 -7.0, 写成二进制是 -111.0,相当于 -111.0
那么 S = 1,M = 1.11,E = 2
那么这个跟浮点数在内存中的存储有什么关系呢?
IEEE 754规定:
对于32位浮点数(float),最高一位储存符号位S,接着后8位储存指数E,剩下的23位储存有效数字M。
对于64位浮点数(double),最高一位储存符号位S,接着8位储存指数E,剩下的52位储存有效数字M。
这里就展示一下float的储存。
4.2.1浮点数的存储过程
除了上面的基础规则外,IEEE 754还对有效数字M和指数E有一些特殊的规定。
在前面的规定中我们可以看到,M是有取值范围的,1 <= M < 2,那么也就是说,对于所有的浮点数的二进制位,一定可以写成 1.xxxxxxx的形式,其中xxxxxxxx是小数部分。
如果前面的数一定是1,那还储存它干什么,不是浪费了一个比特位的空间。
所以规定计算机在储存时,会自动忽略掉前面的1,只储存后面的小数部分。比如1.11,储存的时候就只储存11,在使用的时候将前面的1添加上。
这样的目的就是为了节省一位有效数字,可以提高精确度。
对于指数E,首先要知道的是,E是一个无符号整数,就是E只能是正数
我们刚刚说过在32位下,E占8个比特位,那么就意味着E的取值范围是 0~255;如果是11位,那它的取值范围是0~2047。
但我们都知道,科学计数法中的E是可以出现负数的,那E只能是正数,那该这么解决呢?
这里IEEE 754规定,存入内存时的E的真实值必须再加上一个中间数,对于8位的E,加上的是127,对于11位的E,加上的是1023。比如,2^10,E是10,那么在储存到内存中时就是10 + 127 = 137,就是 10001001。
我们就以7.0为例来演示一下
我们看看内存中是不是这样的
我的电脑是小端储存,那么实际上的数是:0x 40 e0 00 00
可以看到算出来的数字跟我们推断出的数是一致的。
4.2.2浮点数取出的过程
对于S和M,在取出的过程中跟存入内存时是反过来的,直接逆着理解就行。但是对于指数E,还有一些特殊规则。
E不全为0或全为1
这时,取出的过程就跟存入的过程是完全相反的过程,直接反着来就行,即将指数E的计算值减127(或1023),得到真实值,再将有效数字M第一位加上1。
就比如这样一个二进制序列
很明显S = 0,M = 0, 我们计算出来的E = 126,那么真实的E就是126 - 127 = -1,那么这个数还原出来就是 (-1)^0 * 1.0 * 2 * (-1) = 0.5。
E全是0
存储的全是0,那么就是说明,真实值加上127之后还全是0,那么真实值就是是-127,就相当于这个数最后是乘了个 2^(-127),那么这个数肯定是非常非常非常小。
那么这个时候就规定E的真实值是 1-127(或1-1023),就是-126,但是有效数字前面不在加上那个1,直接表示为 0.xxxxxxx的小数。这样做是为了表示 +-0,就是接近于0的很小的数字。
我们看这样一个二进制序列
很明显S = 0,M = 01,E全是0,那么按照我们刚刚所说的,E = 1 - 127,那么这个二进制就表示为 (-1)^0 * 0.01 * 2^(1-127),可以看到,这是一个非常非常非常小的数。
E全是1
我们刚刚说过E的取值范围是0~255,那么如果全是1的话,真实的E就是255 - 127 = 128,那么随后还原出来就相当于乘一个 2^128,那么这是一个非常非常非常打的数字了,所以如果E全是1,就是用来表示 +-无穷的(正负取决于S)。
4.3题目解析
了解完浮点数在内存中是如何储存的,那么我们回到刚刚的题目,先来看第一个环节
为什么*pFloat的值是0.00,*pFloat是一个指向float的指针,我们将n的地址取出来,将它强制类型转换为float*,随后我们以 %f 的形式打印,我们知道 %f 是打印浮点数的,那么你以 %f的形式来打印,程序就会认为*pFloat中的值是一个浮点数的值,那么程序就会把n的二进制序列当成是以浮点数的形式来储存的。
那么我们来看n的二进制序列是什么样的
如果将它看成是浮点数的储存形式
这样看的话很明显 S = 0,E全是0,M也很小,那么以浮点数的形式来看的话,这个就是一个很小很小的数,所以打印出来的就是0。
了解完第一个,我们来看第二个环节
这回*pFloat我们让它变成了一个浮点数9.0,那么它的二进制序列就是 1001就等价于 1.001 * 2^3,那么就很明显看出 S = 0,M = 1.001,E = 3 + 127。计算E的二进制序列是0111 1111,那么9.0在内存中就是这么储存的
然后我们看到,在第一个printf中,要求用%d的形式来打印n的值,%d是用来打印整形的,你既然是让我用整形的方式来打印,那么我就把你当作是一个整形,那就直接把内存中储存的二进制序列给打印出来了,就是刚刚上面的那一串。
我们掏计算器来算一下