目录
引言:
先前我们已经讲过了原码反码和补码,也知道整数存储在内存中是以补码的形式存储的,所以整数在内存中的存储我们这边就直接浅提一嘴,主要是拓展讲解大小端字节序和大小端字节序的判断以及浮点数在内存中的存储( 为什么拓展这些呢,因为你们肯定会有一些疑问比如为什么一个数明明十六进制是11223344存进去,但内存里是44332211显示,又比如为什么同一个地址,用浮点类型解引用的得到的值却不一样了**),**经过这篇的讲解后,你们就会顿悟了
该篇的内容除开整数在内存中的存储,其他部分其实算是拓展了,因为讲解的内容会比之前深入挺多,那么,话不多说,接下来我们进入数据在内存中的存储详解正篇
1.整数在内存中的存储
在讲解操作符和指针的时候,我们已经讲解过了以下内容,这里就不过多赘述了
1.整数的二进制表示方法有三种,原码反码和补码
2.三种表示方法均有符号位和数值位俩部分,符号位0表示正,1表示负
3.正数的原反补都相同,负数的原反补都不相同
4.在内存中存储的是整数的补码
5.负数要得到补码就是原码取反+1,若是补码想要得到原码,就是补码取反+1
其实这部分知道后,整数在内存中的存储基本你们也都知道了
那么,既然内存里存的是补码,为什么存进去是11223344,但我们调试时候观察内存却会发现从低地址到高地址显示的数是44332211呢,这个就涉及大小端字节序了,接下来我们来详细讲讲大小端字节序以及字节序判断
2.大小端字节序和字节序判断
首先,我们先来看如下这个代码
cpp
#include<stdio.h>
int main()
{
int x = 0x11223344;
return 0;
}
我们调试这个代码,来观察内存中x的11223344是怎么存放的,如下图

我们可以发现,x的值再内存中是倒着存储的,这是为什么呢,这就涉及了大小端
2.1.什么是大小端
其实超过一个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为大端字节序存储和小端字节序存储,下面是具体的概念
大端字节序存储:是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容保存在内存的低地址处
小端字节序存储:是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容保存在内存的高地址处
上述概念需要记住,这样方便分辨大小端,当然 ,可以用自己的方式来辅助记忆
比如异大同小(低地址放的是低字节就是同,如果低地址放的是高字节就是异)
我们来对数据进行分析,来帮助我们记忆这俩种存储方式,以11223344为例,44是低位字节,11是高位字节,我们都知道,内存是从低到高的,那么如果低地址放的是11,也就是说低地址放了高位字节,那么就是大端,如果低地址放的是44,也就是说低地址放了低位字节,那就是小端
那么,我们了解大小端后,我们来讲讲为什么会有大小端
2.2.为什么有大小端
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元对应这一个字节,但C语言中除了占一个字节的char之外,还有占2个字节的short,占4个字节的int等等,另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题,因此就导致了大端存储模式和小端存储模式
例如:一个占2个字节的short类型x,在内存中的值为0x0010,x的值为0x1122,那么0x11为高字节 ,0x22为低字节,对于大端模式而言,0x11要放在低地址中,0x22要放在高地址中,所以如果调试内存的话,我们可以看到是11 22,对于小端模式而言,0x11要放在高地址中,0x22要放在低地址中,所以调试内存的话,我们可以看到是22 11
注:虽然大小端的存储方式不一样,但读数据时候读出来的数是一样的
我们现在所用的VS就是小端模式,当然也有大端模式的IDE,还有些ARM处理器还可以由硬件来选择是大端还是小端模式,这里就不过多赘述了,接下来我们来练习一下
2.3.练习
2.3.1.练习1
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序----百度笔试题
大端字节序和小端字节序的概念这里就不重复赘述了,上面刚讲过,那么我们来设计一个函数来实现判断当前机器字节序的功能
首先,我们可以想一想如果是一个int类型的1,放在内存里会是什么样子的
如果是大端的话就会是00 00 00 01
如果是小端的话就会是01 00 00 00
那么,我们只需要创建一个变量,将它初始化成1,然后再取出地址,将地址强转成char*类型再解引用,得到的要么就是1,要么就是0,如果是1就是小端,如果是0就是大端,那么这题是不是就迎刃而解了,那么代码如下
cpp
#include <stdio.h>
int check_sys()
{
int i = 1;
return (*(char *)&i);
}
int main()
{
int ret = check_sys();
if(ret == 1)
{
printf("⼩端\n");
}
else
{
printf("⼤端\n");
}
return 0;
}
2.3.2.练习2
cpp
#include<stdio.h>
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;
}
先来看a,a是char类型,在VS的环境下,char就是有符号类型,a是10000001,补码就是11111111,接下来要输出a,因为是%d,所以要整型提升,因为是有符号类型,所以整型提升的时候就是补符号位,就是11111111111111111111111111111111,接下来再转成原码,取反加1也就得到了10000000000000000000000000000001,所以a输出的就是-1
再来看b,b是signed char类型,因为VS环境下char默认是有符号类型,所以a和b是一样的,所以b分析跟a是一样的,那么同理就是输出-1
再来看c,c是unsigned char类型,也就是无符号类型,-1的原码是10000001,反码就是11111111,也就是说c存进去的是11111111,最高位的1不是符号位而是数值位,接下来对c进行整型提升,因为是无符号类型,所以直接补0,所以最后就会得到00000000000000000000000011111111,所以输出就是225
所以这个代码输出就是-1,-1和225,我们来看运行截图

2.3.3.练习3
cpp
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
在讲这个前,我们要先知道char类型存的数据范围是多少,首先我们先来看八位二进制有几种存放形式,如下图

我把他分为了俩部分,正数部分是符号位为0的,负数部分是符号位为1的,所以虽然0是零,但我把他归类到了正数部分
那么,正数的最大是多少呢,就是01111111,即127,所以非负数的范围是0到127,那么,负数部分因为内存里存的是补码,所以我们要把转成原码,如下图这部分其实很简单,就是-1到-127

主要需要注意的是10000000这个,这个转成原码时候,可以发现,补码是11111111,加1后一直进位,如果符号位放在另一个维度,就变成了10000000,所以我们规定10000000就是-128
所以有符号类型的char的范围就是-128到127,那么接下来我们来看上面那个代码
因为a是-128,所以存放在内存中的补码就是10000000,随后因为要输出a,所以先整型提升,变为 11111111111111111111111110000000,因为输出格式是无符号类型,所以最高位就是数值位了我们可以用计算机来算一个这个值,这个值就是4294967168
我们运行一下就可以发现确实和我们推理的输出是一样的,如下图

2.3.4.练习4
cpp
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n", a);
return 0;
}
接下来我们来看这个代码,和上面那个很相似,首先128是10000000,存到a中就是100000000,因为a是char类型,默认为有符号类型,所以最高位的1就成了符号位,所以输出时候对a进行整型提升就变成了11111111111111111111111110000000,这也就跟上一个练习的补码一样了,因为是输出无符号,所以补码即原码,输出的值就和练习3的结果一模一样,如下图

2.3.5.练习5
cpp
#include <stdio.h>
int main()
{
char a[1000];
for (int i = 0; i < 1000; i++)
{
a[i] = -1-i;
}
printf("%d", strlen(a));
return 0;
}
首先,我们看这个代码,输出的是strlen,所以我们需要先找到0第一次出现是在哪个下标,那么多少的情况下截断后会是0呢,那便是最低位的8个字节全是0的情况,因为是补码,所以最小的截断变成0的是11111111111111111111111100000000,这是个补码,那么这个的原码就是10000000000000000000000100000000,也就是-256,那么,在-256的情况下,i是几呢,就是255,0到254有255个元素,所以输出会是255,我们来看下 运行结果,如下图

当然,也有另一种方法来得到答案,首先,你们可以推一下,便可以得出
对有符号类型的char而言,每次加一就是从00000000到11111111,那么就是从0-127,再从-128到-1,这是为什么呢,因为10000000是-128,,然后越加越小,因为内存里存的是补码,要转成原码,然后对11111111再加1,就会进位,裁断时候高位就没了,只剩下了00000000,也就是0,所以如果是每次加1的话,那就是从 0到127,再从-128到-1的循环,如果是减1的话就是反着来,因为一开始是-1,然后要到0,那就是反着绕一圈,要减255次,因为循环里第一次i是减0,所以就是254+1=255次,也是一样的
2.3.6.练习6
cpp
#include <stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
如果上一题懂了,这题就很简单了,因为i是无符号类型,所以值的范围是0到255,在循环中i等于 255 后,还会再加一次1,此时二进制就是100000000,因为会发生截断,所以i又成了0,那么,一直往复,就死循环了,所以会一直重复输出hello world,因为是死循环,不好演示,感兴趣的自己调试一下就好了
2.3.7.练习7
cpp
#include <stdio.h>
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
return 0;
}
这题其实跟上题大差不差,当i是0的时候,i再--就是-1了,二进制补码就是11111111111111111111111111111111,因为是无符号整型,所以就变成了一个极大的数,然后又满足条件,就又死循环了,这个我们来看下运行图

2.3.8.练习8
cpp
#include <stdio.h>
#include <windows.h>
int main()
{
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%x,%x", ptr1[-1], *ptr2);
return 0;
}//%#x在打印时候会加上0x
注:这个代码要在x86环境下调试,x64环境下调试会报警告
这个代码的输出ptr1很简单就能推出,跨过整个数组然后转成int*类型,然后再是输出ptr1[-1]就是往前一个整型内存然后输出就好了,也就是4,这个在指针那就讲过了,就不多讲了
主要难的是ptr2,ptr2的地址就是将a的地址强转成了int类型,所以加1就是加一,然后再强转成int*类型,此时,ptr2的位置其实是在第一个int所占内存的中间,因为VS是小端字节序 ,所以这里就直接讲小端字节序了,首先,我们要知道a数组存在内存中是这么存的
01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 00
对int类型的a地址加1就相当于是指向了如图这里,因为强转成了int*,所以解引用时会包括后面的四个字节

那么输出就自然而然就是ptr1是4,ptr2是2000000(%x是十六进制表示),如下图

3.浮点数在内存中的存储(拓展)
简单的浮点数我们都知道是什么,这里拓展下一种新的浮点数
我们都知道1e5是1*10的5次,那么,1E5是什么呢,就是1.0*10的5次
浮点数表示的范围在float.h中定义
整数表示的范围在 limits.h中定义
那么,讲完基础的后,我们来讲讲浮点数在内存中是怎么存储的,首先我们先看下面这个代码
cpp
#include <stdio.h>
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;
}
我们来看输出的结果和你想的一不一样

为什么输出会和我们想的不一样呢,说明浮点数和整型在内存中存储的方式是不一样的,因为都是占4个字节所以不可能会是内存的关系,只可能会是读取内存中数据时的方式不一样
3.1.浮点数的存储
上面的代码中,n和*pFloat在内存中命名时同一个数,为什么浮点数和整数的解读结果会差别这么大
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法
根据国际标准IEEE754,任意一个二进制浮点数V可以表示成下面的形式,如下图

所以浮点数的存储,其实存储的就是S,M,E相关的值,S其实跟整型的符号位一样,0为正数,1为负数
举例来说:
十进制的5.0,写成二进制就是101.0,相当于1.01*2的2次,那么按照上面V的格式,可以得出S = 0,M = 1.01,E = 2
对于32位的浮点数,最高的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M(也就是float类型)
对于64位的浮点数,最高的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M(也就是double类型)
我们看下图 ,可以理解的更清晰

3.1.1浮点数存的过程
IEEE 754对有效数字M和指数E,还有一些特别的规定
前面说过,1<=M<2,也就是说,M可以写成1.xxxx的形式
IEEE 754规定。在计算机内部保存M时,默认这个数的第一位总是1,所以是可以被舍去的,只保存后面的xxx部分,如果保存1.01的时候,只保存01,等到读取的时候,再把第一位的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。
3.1.2.浮点数取的过程
指数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的很小的数字。
0 00000000 00100000000000000000000
E全为1
这时,如果有效数字M全为0,表示±⽆穷⼤(正负取决于符号位s);(因为2的255-127次也就是2的128次已经大的很夸张了)
0 11111111 00010000000000000000000
关于浮点数的表示规则就说到这里,接下来我们来回看刚刚的题目
3.2.开始的题目解析
首先,存的是9,所以存的二进制序列就是0000 0000 0000 0000 0000 0000 0000 1001 ,所以S就是0,E就是00000000,M就是000 0000 0000 0000 0000 1001
由于指数E全为0,所以符合E为全0的情况。因此,浮点数V就写成:
V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146) 显然,V是⼀个很小的接近于0的正数,所以用十进制小数表示就是0.000000。
接下来用float类型来改值,9.0就是1001.0,也就是1.001*2的3次,那么E要加上127也就是130,因为是正数,所以S是0,E是130(10000010),M是 001 0000 0000 0000 0000 0000
那么这个32位的二进制我们转成十进制看下是多少,如下图

这也就完美诠释了前面的代码是如何出来的神奇结果
结语:
该篇主要是修炼内功,知道底层才好在之后对难点进行剖析
希望以上内容对你有所帮助,感谢观看,若觉得写的还可以,可以分享给朋友一起来看哦,毕竟一起进步更有动力嘛
