目录

C语言数据在内存中的存储

[C语言]数据在内存中的存储

一、整数在内存中的存储

在讲解操作符的时候,我们就讲过了下⾯的内容:

整数的2进制表⽰⽅法有三种,即原码、反码和补码。

有符号的整数,三种表⽰⽅法均有符号位和数值位两部分,符号位都是⽤0表⽰“正”,⽤1表 ⽰“负”,最⾼位的⼀位是被当做符号位,剩余的都是数值位。

正整数的原、反、补码都相同。

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

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

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

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

在计算机中整型数据就是以反码的形式存储的

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

在讲大小端字节序之前我们先看一个案例:

https://i-blog.csdnimg.cn/direct/569dc3b35ac9412ab74d281ee058ec6c.png

我们给变量a赋一个16进制的数,我们通过调试内存窗口看看其在内存中的存储:

https://i-blog.csdnimg.cn/direct/b84538a1ba8443a89b1f08dc67fce760.png

可以看到其在内存中是44->33->22->11的存储方式,可以看到其是按照字节为单位倒着存储的,这是为什么呢?

下面我们就来看看今天要看的内容,大小端字节序:

那么大小端字节序是什么呢?

当超过一个字节的数据在内存中的存储是有顺序问题的,按照不同的存储顺序,可以分为大端字节序和小端字节序存储

大端: 指数据的低位字节内容保存在内存的高地址,而数据的高位字节内容,保存在内存的低地               址处

小端: 指数据的低位字节内容保存在内存的低地址,而数据的高位字节内容,保存在内存的高地               址处

那么我们上面的案例可以看到,11是变量a的高位字节的内容,其在内存中的地址也是高位的。那么我们当前机器的环境的存储方式是小端存储。

那么也就是说不同的机器其对于数据的在内容中的存储方式是可能不同的,那么我们该如何通过代码的方式来判断当前机器的大小端序呢?

大小端字节序的判断:

我们创建一个变量a给其赋值1那么其在内存中的存储如下:

大端:00 00 00 01

小端:01 00 00 00

可以看到我们在存放1的时候,大端的第一个字节是00,小端的第一个字节是01。其他位置就没区别,那么我们可以单独比较第一个字节的内容,那么我们可以使用一个字符指针,访问其第一个字节的内容,对其解引用。如果是1那么就是小端字节序,如果是0那么就是大端字节序。

如下:

https://i-blog.csdnimg.cn/direct/d95bbf42b1b44481a9cb25c69bd96168.png

下面我们看看其运行结果:

https://i-blog.csdnimg.cn/direct/b354057bc6e34025b8433c6160744bdd.png

可以看到vs就是小端字节序。

三、练习

练习1:

我们对上面的代码进行分析:

首先就是我们创建了一个字符型变量a,然后对其赋值-1,然后我们知道的,在计算机的内存存储中对整型数据是存放其补码,那么我们先求其补码。

-1:

原码:10000000  00000000  00000000  00000001

反码:111111111  111111111  111111111  111111110

补码:111111111  111111111  111111111   111111111

但是此时a是字符型,那么其实际是只有一个字节的内容,所以此时的a存放的内容:11111111。

下面我们看b,b是一个有符合的char字符,那么其和a的情况是一样的,那么b实际存放的内容是:11111111。

最后我们分析c,c是一个无符号字符,不过我们赋值的时候是赋值了-1,我们知道赋值运算符的计算是先算右边的结果然后再赋值给左边,那么此时的c还是-1,然后其是char型,那么其现在实际存放的内容是11111111;然后其又是无符号的char型数据,那么其所有的二进制都是值,没有符号位。

然后我们这里打印的占位符用的是%d,即有符合整型的打印,那么此时要对其进行整型提升,将其提升至4个字节。那么我们对a和b的整型提升如下:

反码:11111111  11111111  11111111  11111111

原码:10000000  00000000  00000000  00000001

那么a和b的以%d的形式打印的结果是-1。

然后我们看看c的整型提升,那么对于无符号的高位补0,那么其整型提升如下:

补码:00000000 00000000 00000000 11111111

原码:00000000 00000000 00000000 11111111

因为是正的,那么其三码都是一样的,那么c使用%d打印的结果就是255了。

运行结果:

https://i-blog.csdnimg.cn/direct/f55d1d19b77b4bb1a1903cc863b05b8d.png

练习2:

https://i-blog.csdnimg.cn/direct/0168c25d0f9841a9a062c974b5508cfc.png

通过第一个练习我们对这种类型的题目有了一定的了解了,下面我们进行分析,首先是对与字符a和上面的一样,先求其补码

a:

原码:10000000 00000000 00000000 10000000

反码:11111111   11111111   11111111   011111111

补码:11111111    1111111    11111111   10000000

然后进行截断,那么此时就为:10000000。

下面我们看变量b

b:

因为b是一个正数,那么其三码是一样的

补码:00000000 00000000 00000000 10000000

然后截断,那么b存放的也是10000000

然后其是以%d形式打印,也就是无符号整型的打印,然后要进行整型提升,高位补符号位。

那么a和b的整型提升为:

11111111 11111111 11111111 10000000

然后又因为其是无符号整型,那么其所有位都是表示的值,那么我们通过计算器计算的结果为:4394967168

下面我们运行看其结果:

https://i-blog.csdnimg.cn/direct/cd7b5b8be19643fe8abb23d3cdfc2513.png

可以看到运行的结果和我们分析的一致。

练习3:

https://i-blog.csdnimg.cn/direct/8313c892ea8b47978dc68e9ea126ff34.png

这里需要对char的内容有一定的了解,char默认是有符合的字符,它的取值范围是-128~127。

在有符号字符型中有两个特殊的值:

1:00000000

2:10000000

按照有符号的计算,那么第一个就是0,那么第二个就是-0,但是不存在-0,那么我们认为规定了-128。

下面我们对上面的练习进行分析:

首先我们知道strlen函数的使用,是从传入的参数的地址开始往后找‘\0’然后返回前面的字符个数,

然后我们看这个循环,其从字符数组开始对其从-1开始赋值,然后到-129后,由于此时超过了有符号字符型的范围,那么我们看看-129的补码。

-129:

原码:10000000 00000000 00000000 10000001

反码:11111111   111111111  11111111   01111110

补码    11111111   11111111   11111111   01111111

然后对其截断:01111111

那么此时就变成了127,可以发现这个127就是有符合字符型的最大值,是不是很不可思议,然后循环又从127开始进行-1对字符数组进行赋值,然后变成0,然后循环结束。

虽然循环是进行了1000次才结束,此时字符数组中是有1000个字符,但是strlen函数碰到0就结束就算了,那么关键就是第几次回到0,先从-1到-128,然后从127到1,那么一共经过128+127=255个数。

下面我们看看运行结果:

https://i-blog.csdnimg.cn/direct/000163dc6d164c84b5183436c0a91873.png

练习4:

https://i-blog.csdnimg.cn/direct/2a3c8ed575a949ebbcb017ac642f7d3c.png

我们先对上面的代码进行分析:

上面首先先创建了一个全局变量a,且其是一个无符号字符,那么其范围为:0~255,那么其加到255这部分还没问题,当其到255后再+1,此时是什么情况呢?

那么我们看其为256的时候在变量a中是什么样子的。

256:

因为是无符号的,所以三码是一样的:

补码:00000000 00000000 00000001 00000000

然后截断:00000000

我们发现256存放到无符号char后就变成了0,然后就又开始进行循环,那么就陷入了死循环。

我们看看其运行结果:

https://i-blog.csdnimg.cn/direct/da05e6ade6964eb09c62b5a188c2912b.png

可以看到其在一直打印hello world。

练习5:

https://i-blog.csdnimg.cn/direct/ffc35bd623624643b0022771e4451e7e.png

这里和上面练习4有点类似,不过这里是无符号整型,然后冲0减到-1,那么我们看看-1在变量i中是咋样的

-1:

原码:10000000 00000000 00000000 00000001

反码:11111111   11111111   11111111   11111110

补码:11111111   11111111   11111111    11111111

那么此时i实际上是无符号整型的最大值,而且其没有符合位,我们使用计算器计算知道其 又开始从:4294967295开始又-1,一直死循环。

运行结果:

https://i-blog.csdnimg.cn/direct/b7dd1a1a02954c4f928d690ff5857977.png

可以看到其一直在打印,而且是从4294967295开始。

练习6:

https://i-blog.csdnimg.cn/direct/82c470317f9c4b779d7b85f538fb23c0.png

这道题就有点难度了,因为这里结合了前面指针的知识,然后这里的环境要求在x86,小端序下,刚刚好vs符合这个要求。

下面我们对这个代码进行分析:

首先就创建了一个整型数组,那么其在内存中的存储情况如下:

01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00

然后我们看指针变量prt1其是在对&a+1,那么其是跳过的整个数组,那么其地址在最后一个00后一个字节,对于第二个指针变量,其是对a数组的首地址进行+1字节的操作,那么其里面实际保存的是00 00 00 02,然后我们看其打印函数的参数,prt1[-1]等同于*(prt1-1),那么此时的地址在数组a的第四个元素处,然后看第二个参数,*prt2存放的是00 00 00 02,然后由于是小端序,而且是由十六进制的形式打印,那么打印结果应该为:4,2000000。

运行结果:

https://i-blog.csdnimg.cn/direct/08a208c3cf91436f86f8a7e6b4de869c.png

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

我们常见的浮点数有:3.1415,1E10等,浮点数包括如下几种:

float,double,long double类型。

下面我们通过一个案例来学习:

https://i-blog.csdnimg.cn/direct/65cf1c16ad77469a9495dd0d367ee964.png

第一个printf的输出很明显就是9了

第二个的话,首先我们分析pFloat是啥,首先其存放的是变量a的地址强制转换为float类型的地址,但是我们不清楚float类型的数据在内存中是如何存储的,那么我们要去了解这个内容才可以分析了。

第三个的首先先将指针指向的地址的内容改成了9.0,那么我们还要看其是如何转换的。

第四个的话是对pFloat指针进行解引用,然后以浮点型的形式打印,那么其打印结果也就是9.0。

下面我们看看浮点数在内容中是如何存储的:

浮点数在内存中的存储其实是有一个公式的吗,这个公式和我们初中的时候学习的科学计数法有点类似。

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

https://i-blog.csdnimg.cn/direct/4d9c4763583449afbd1d484f372201a5.png

例如:一个浮点型数:5.0

那么其转换为二进制为:101。

然后按照上面的公式就为:

S为0,M为1.01,然后E为2

IEEE754规定:

对于32位的浮点数,最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M

如图:

https://i-blog.csdnimg.cn/direct/ab93192c711a431e894724e9e5d41f52.png

对于64位的浮点数,最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字

M

如图:

https://i-blog.csdnimg.cn/direct/7e29a777315a410fbd426d35a5470f8c.png

浮点数存储的过程:

IEEE 754对有效数字M和指数E,有一些特别的规定

上面说到,1<=M<2,也就是说,M可以写成1.xxxxx的形式,其中xxxxx为小数部分。

IEEE规定,在计算机内部存储浮点数的时候,默认第一位总是1,那么可以舍弃,然后就只保留小数部分。

例如:计算机在存储1.01的时候,只保存01,然后等到需要读取的时候,在将第一位加上去,这样的目的是可以节省一位有效数字马。

然后对于指数E,情况就比较多了,首先就是无符号整数,那么此时就说明其存储中不需要符号位,那么其范围也就更大。

不过我们也知道的是,指数部分是可以有负数的,所以IEEE754规定,存入内存的时候E的真实值必须加上中间值。

浮点数取的过程:

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

加上中间值后E不全为0或不全为1:

那么此时浮点数就用下面的规则表示:

就用指数的计算值减去127(或1023)的中间值,得到真实值,然后再将有效数字M前加上第一位的1。

例如:0.5的二进制为0.1,由于规定,整数部分必须为1,那么则变成1.0*2^(-1),其阶码为:

-1+127(中间值)=126,表示为:01111110

而尾数1.0去掉整数部分为0,然后补齐到0到23位:00000000000000000000000

其二进制表示如下:

0       01111110     0000000000000000000000

S             E                               M

此时E不全为0或1,是126 ,那么在取出的时候要减去中间值,然后将M减去的1加回来。

加上中间值后E全为0:

我们知道32位的中间值是127,E加上了127后还是全0,说明此时原本的E,也就是真实值就是-127,想象一下2^(-127),是一个非常非常小的数,无限接近于0了,然后再乘以我们的M也无济于事

所以此时有效数字M不再加上第⼀位的1,⽽是还原为0.xxxxxx的⼩数。这样做是为了表⽰±0,以及接近于0的很⼩的数字

加上中间值后全为1:

我们知道32位的中间值是127,E加上了127后是全1,也就是255,它原本的E,也就是真实值就是128,说明这是一个无限接近于无穷大的情况

所以如果有效数字M全为0,表⽰±⽆穷⼤(正负取决于符号位s)

知道了浮点数的存储规则我们继续看上面的案例吧:

https://i-blog.csdnimg.cn/direct/6197b4465f9a4e0b833f42a264b7f185.png

我们看第二个printf,我们从浮点数的存储来分析:

n是正数,那么其三码是一样的。

补码:00000000 00000000 00000000 00001001

然后我们从浮点型的角度来看:

0       00000000     00000000000000000001001

S             E                            M

然后这里就是E的部分全是0,那么说明这个数字非常小,那么在还原的时候就不会还原M前面的整数1,那么我们将其代入公式看看:

(-1)^00.00000000000000000010012^(-127);

可以看到这个数是非常小的了,那么计算机在打印的时候就只会显示0了。

然后我们看第三个printf

我们先将9.0转换为二进制:

1001

然后其SME如下:

S:0

M:1.001

E:3

然后加上E加上中间值就是130

那么其按浮点数的二进制为:

0    10000010    00100000000000000000000

S          E                            M

然后其要以整数的形式打印,那么其打印的值使用计算器算得:

1091567616

那么我们运行看看其结果是不是我们分析的这样:

https://i-blog.csdnimg.cn/direct/1d7c80c101a14a3087624a97f4e92482.png

可以看到其打印的结果和我们分析的一样。