深入分析数据在内存中的存储

我们知道,计算机的本质工作就是计算。而实际的计算中就避免不了接触和处理大量的数据,那么数据有什么类型呢?数据在内存中的存储又是什么样的呢?计算机是如何对数据进行计算和处理的呢? 下面就让我们来一探究竟,揭开数据在计算机里存储的神秘面纱吧。

1.数据的类型

数据的类型是十分重要的,因为数据的类型直接决定了计算机存储它们的方式。对于C语言来说,数据的类型大致分为如下几种:

1.整型家族:

 这时候你可能就会疑问,为什么char字符型也被归算到整形家族呢? 事实上,字符类型确实也是整形的一种。一个字符会对应一个Ascii码,在计算机的内存中,字符变量被解析成对应的Ascii码进行存储,所以说字符类型(char)也是一种整型。

对于C语言来说,整型家族的成员的符号遵循这一个原则:

浮点数家族:

 自定义数据类型:

 对于复杂的数据类型,我们暂且不做深究,接下来我们深入研究整型和浮点型家族的内容。

整型家族:

前面我们列举了整型家族的成员,有的同学可能就会好奇,为什么char类型也被分到了整型家族中去?因为字符型常量在计算机种存储的就是char的Ascii码值!所以我们也认为字符型是整型。

这时候有的同学突发奇想,用一个无符号类型的整数a存储-1并打印i的值。

#include<stdio.h>
int main()
{ unsigned int a=-1;
  printf("%u\n",a);//打印无符号数用%u
return 0;
}

可能你会想,打印出来的可能是1。但真的是这样吗?运行结果如下:

 天啊,怎么会打出这么大的数字?接下来我们就要剖析-1到底在计算机里是怎么存储的。

1.原码,反码,补码

这里以有符号数-1为例,我们知道-1是int类型,占4个字节,1个字节有8个比特位。所以一个整型数字是由32个01二进制序列组成的。

原码的构成:32个比特位,第一位是符号位,0代表是正数,1代表是负数。规定完符号为后,把这个数的绝对值转化成2进制,用0补齐就是这个数的原码,就拿-1来说。

10000000000000000000000000000001//-1的原码

 第一个1就是它的符号位。

那么内存里真的是存储原码的吗?并不是!为什么呢?如果是,不妨考虑下面这个计算。

计算:1-1 我们都知道1-1=0,但其实对于cpu来说,它只有加法器,所以1-1会被cpu解析成

1+(-1),如果是存储原码,我们不妨使用原码进行计算:

00000000 00000000 00000000 00000001--->1的原码

10000000 00000000 00000000 00000001--->-1的原码00

相加得:

10000000 00000000 00000000 00000010--->相加的结果

你会发现,无论符号位是否参与运算,最后的结果显然都不是正确的,说明计算机存的并不是对应数字的原码。事实上,计算机存储的是补码。那么原码是怎么变成补码的呢?

事实上,原码先变成反码后再变成补码,具体规则如下:

对于正整数来说,原码 反码 补码均相同。

而对于负数来说,原码 反码 补码遵循下面这个规则:

反码:原码符号位不变,其余位按位取反。

补码:反码+1。

以-1为例:

我们接着用补码来计算1-1的结果:

 用补码计算得到了正确的结果,也就可以说明计算机存储的是补码。

同理我们也可以解释为什么以无符号整数形式打印-1会得到那么大的结果了,我们打印使用的是原码,而对于无符号整型来说,内存中的补码全都被解析成有效位,我们用计算器输入32个1组成的二进制序列转换成10进制整数的结果和运行结果进行对比:

 

结果一模一样,证实了计算机存储的是补码!

2.截断和整型提升:

我们知道一个整型的大小是4个字节,一个char类型的元素大小是1个字节,如果我要把整型放到字符型会发生什么呢?

以200为例:

 如图,因为char 只能存八个比特位,所以只有最后的8个比特位的内容被保留了下来。

2.整型提升:

前面我们知道了将一个整型放到一个字符型只会截取最低的8个比特位,那么下面这段代码会发生什么呢?

#include<stdio.h>
int main()
{   char a=128;
   printf("%u\n",a);
return 0;
}

打印结果如下:

 我们来简单分析一下运行结果,首先我们知道128是一个整数,我们写出它的二进制序列:

00000000000000000000000010000000----->128的原码
放到char类型发生截断: 100000000

而我们用%u将a看做无符号整型,但是a不够4个字节,所以要发生整型提升:

整型提升原则:如果原来的数是有符号位,则高位不符号位,如果原数是无符号数,高位补0

在vs环境下,char类型默认是有符号类型,那么最高位就是符号位。

这里的128发生截断后的二进制位是:100000000

根据整型提升的原则,用符号位的数字填充高位得到:

11111111111111111111111110000000--->整型提升之后的a的补码

  使用计算器计算结果和运行结果对比:

 结果一摸一样,说明如果我们在打印的时候用%d %u来解析字符型的变量,这个字符型的变量会发生整型提升!

接下来看一段神奇的代码:

#include<stdio.h>
#include<string.h>
int main()
{   char a[1000];
    int i;
   for(i=0;i<1000;++i)
{
    a[i]=-1-i;
}
  printf("%d\n",strlen(a));
    return 0;
}

运行结果如下:

 为什么不会是1000呢?我们来简要分析一下:

我们知道char类型是8个比特位,那么8个比特位可能的2进制序列如下:

00000000--->0

00000001--->1

00000010--->2

.......

011111111--->127

......

10000000--->-128(人为规定,为了和0000000/区分,127+1--->-128)

10000001--->-1(符号位是1)

10000010--->-2

......

111111111--->-127

00000000--->0(+1,最高位被丢弃,结果还是0)//陷入了循环!

我们知道,strlen碰到'\0'才能结束,那么我们就来看看什么时候碰到‘\0’:

 首先程序从-1开始递减到-128--->执行了127次

-12*-1--->127(127+1==-128)

接着从127--->0执行了128次

所以最终长度=127+128=255

我们可以用这样一个图来表示这个过程:

 其实对于任何一种数据类型,C语言都规定了每种类型的最大最小值,可以用头文件<limits.h>和

<float.h>查看对应数据类型的上下界。

 整型家族的最大最小值:

 浮点型家族最大最小值:

2.大小端字节序:

前面我们知道了数据在计算机内部存储的是补码,那么我们怎么真实观察内存中的数据存储呢?

使用vs的内存窗口就可以观察:下面观察10在内存中的存储:

 因为用2进制表示太长,所以内存中使用16进制来存储:4个2进制位就是一个16进制位

10的2进制序列:0000 0000 0000 0000 0000 0000 0000 1010

16进制序列: 0X00 00 00 0a

内存存储: 0a000000 

计算机存储的确实是补码,但好像并不是直接存它的补码,而是用一定的方式存进去,这就是

我接下来要介绍的大小端字节序存储:

对于多余1个字节的类型数据,计算机的存储模式分别有两种:大端,小端存储

小端存储:把低字节的内容放在低地址的空间,把高字节的内容放在高地址的存储方式。

大端存储:把高字节的内容放在低地址的空间,把低字节的内容放在高地址的存储方式:

接下来我们在以-10为例来看-10在内存究竟是大端还是小端存储方式

10000000 00000000 00000000 00001010--->-10的原码

11111111    11111111  11111111   11110101--->-10的反码

11111111     11111111  11111111   11110110--->-10的补码

0xff ff ff f6 ---->补码的16进制表示法

如果是大端:存储的结果应为: 0xfffffff6

如果是小端:存储结果应为:0xf6ffffff

打开窗口调试结果如下:

 和小端存储模式预期结果相同,说明这台计算机的存储模式是小端模式:

那么怎么通过程序来验证呢?

假设我们放入一个1,如果是小端存储,那么小端字节的第一个内容就是1,假设是大端存储则是0

所以我们只需要访问第一个字节的内容,而我们知道指针的类型决定了它解引用能够访问的字节内容,我们只要一个字节,所以我们需要一个字符指针。

代码如下:

int check_sys()//小端模式返回1,大端返回0
{
	int i = 1;
	char* pi = (char*)&i;//使用字符型指针访问1,解引用只能访问1个字节
	if (1 == *pi)
	{
		return 1;
	}
	return 0;
}
int main()
{   
	if (check_sys() == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

运行结果如下:

注意!,大小端字节序对char类型的变量没有任何意义!因为char类型只有1个字节,就没有什么字节序可研究!

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

有如下的一段代码:

#include<stdio.h>
int main()
{   int n=9;
   float* pFloat=(float*)&n;
    printf("n的值是%d\n",n); //预期结果 9
 printf("*pFloat的值是%f\n",*pFloat);//预期结果9.0
 *pFloat=9.0;
printf("nums的值为:%d\n",num);//预期结果9
printf("*pFloat的值是%f\n",*pFloat);//预期结果9.0;
  return 0;
}

运行结果和我们预期一样吗吗? 运行结果如下:

 你会发现 我们只对了一半,但为什么会出现0和1091567616呢?这就和浮点数在内存中的存储方式有很大关系了。

事实上,国际标准协会IEEE(电子和电气工程协会)754对浮点数做出了如下的规定:

对于任何一个二进制浮点数v,都可以表示成下面这个形式:

v=(-1)^s*M*2^E

s:代表的是符号位,0代表正数,1代表负数

M:代表的是有效数字,范围是1.0<=M<2

2^E:代表指数位。

光听定义可能有点抽象,下面我们用十进制的0.5为例来深入理解定义:

0.5是正数---->s=0

0.5=1.0/2->1.0*2^(-1);

所以10进制的0.5表示成2进制的浮点数形式就是  (-1)^0*1.0*2^(-1)。

那么浮点数同样在计算几种也被用二进制存储,下面我们来讲一下32位平台和64位平台浮点数存储的方式。

32位平台:

64位平台 

 注意,因为浮点数的有效数字基本都是介于1-2之间的,则在存储的时候,有效数字中小数点前的1不会被存进内存中,这样数据就能够多存储一位数字。

这里的指数位有点特殊,并不是直接将指数位对应的真实数据存储进入内存,而是加上一个数字在存入内存,对于32位平台,这个数字是127,而对于64位平台则是1023。以32位平台为例:

假设一个数的指数是3

3+127=130,则在内存中存储的就是以130进行存储:

10000010--->对应的就是指数3

这就有可能出现两种极端的二进制序列:

00000000

11111111

假设说一个浮点数对应在内存中的二进制序列是全0,那么真实的指数就是-127

这是一个无限接近于0的数字,所以对于这样的浮点数,不在以1作为开头而是直接把它

表示成以0的浮点数,这样做是为了表示它很小。

同样,如果八位二进制全1,那么真实对应的指数是128,是一个非常大的数字,

这里我们就不在深究这个很大的数字。

接下来我们就应用定义和规律来分析前面代码的结果

 第一步:写出9的2进制序列->1001

第二步:转换成2进制浮点数--->1.001*2^3

第三步:结合定义,得出s=0,E=3,M=1.001

最后写出内存中存储的真实情况:

符号位:0(正数)

指数位:3+127=130--->10000010

有效数字:001(第一位不存)

写出序列

01000001000100000000000000000000--->浮点数9.0在内存中的存储。

同样我们写出整数9在内存中的存储

00000000000000000000000000001001--->整数9在内存中的存储。

接着我们跟着分析代码:

 printf("n的值是%d\n",n);

//用%d告诉计算机我解析的是一个整数,所以结果正确。

printf("*pFloat的值是%f\n",*pFloat);

//%f告诉计算机我解析的是一个浮点数

00000000000000000000000000001001-->浮点数的方式解析

解析结果:指数位全0,真实指数是-127,以0开头-->0.1001*2^(-127)

但是因为%f默认打印是小数点后6位,而有效位在很后面,所以打印了0.000000

 *pFloat=9.0;
printf("nums的值为:%d\n",num);

通过指针改变了num成浮点数9.0,num的内容如下:

01000001000100000000000000000000--->浮点数9.0在内存中的存储。

%d说明我解析的是一个整数

01000001000100000000000000000000--->反码,由于是正数也是原码。

计算器运算结果如下:

 

运行结果对比:

结果相同,证实了浮点数在内存中确实是和我们分析的相同,也就是说即使二进制序列相同,以不同的方式解析数据,最后得到的结果也是不相同的。

四.在循环中慎用无符号类型!

代码如下:

#include<stdio.h>
#include<windows.h>//使用Sleep函数
int main()
{ unsigned int i=9;
  while(i>=0)
{ 

printf("%d\n",i);
 --i;
Sleep(1000);//睡眠函数,有利于我们观察结果

}
return 0;
}

 因为对于无符号数来说补码的全部二进制位都会被认为是有效位,也就是不可能会出现小于0的情况,这时候程序就陷入了死循环中!!

5.布尔类型:

在C99标准中引入了新的类型----bool,bool类型的变量只有两种值,true or false,表示条件的成立与否,在C++和java中使用较多,对于C语言,要想使用布尔类型,需要包含头文件<stdbool.h>

我们知道,C语言用0表示假,用非0表示真,那么布尔类型的true和false的本质是什么的?

我们可以转到<stdbool.h>文档查看:

 可以看到,true和false的本质也是1和0。所以在C语言中,不必特意去使用布尔类型。

最后插入一张有意思的图片:

 任何一个小白都会经历这样的过程称为大牛!希望大家在挺过绝望之谷,共同走向成功!!!

总结:

我们要了解整型家族的成员,浮点型家族。了解整型提升的原理,整型在内存中的存储,浮点型在内存中的存储,制作不易,有错误还望指正。希望大家共勉和努力,共同进步!!!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值