深度剖析数据在内存中如何存储

🔴1.前言

在一开始书写博客时,我直接从数组开始介绍,接下来介绍了指针(可以去专栏里面找),而前面的循环、分支结构等因为逻辑比较简单所以也并没有展开写出来,后续会适当更新相关例题,大家参考C语言书本上的内容即可。而这次博客主要分享整型和浮点型数据在内存中是如何进行存储的,了解C语言中数据类型等,而数据类型中的指针类型与自定义类型等内容本节先不予讨论。

话不多说,直接看下面内容!
在这里插入图片描述

🟠2.数据类型

char
short
int
long
long long
float
double

类型的意义:
(1)使用这个类型开辟空间的大小。
(2)如何看待内存空间的视角。

在数组那篇博客中,我提到sizeof()操作符用来计算字节大小,所以不同类型系统所分配的空间也是不尽相同的。

在定义变量时,我们无非用到上面所列举的类型,而我们知道定义变量时有的变量是非负的,所以接下来也就有了带符号类型——“signed”,和无符号类型——“unsigned”。

整型:

//1个字节
char:
  unsigned char
  signed char
//2个字节
short:
  unsigned short
  signed short
//4个字节
int:
  unsigned int
  signed int
//4/8个字节
long:
  unsigned long 
  signed long

浮点型:

float//4个字节
double//8个字节

空类型:

void表示空类型,通常用于函数的返回类型,函数参数,指针等

🟡3.整型在内存中的存储

我们知道变量在内存中是要开辟空间的,而空间的大小是由类型决定的。有了这个概念,我们接下来看一下整型数据到底在内存中是如何存储的呢❓

💨3.1原码、反码、补码

计算机中,有三种表示2进制的方法,原码、反码、补码,接下来我们来看其定义:

(1)原码:直接将数值按照正负数的形式翻译成二进制就可以得到原码
(2)反码:将原码的符号位保持不变,其余位按位取反
(3)补码:将反码加1即可得到补码

值得注意的是:
(1)正数的原码、反码、补码全都相同,而负数则需要根据上面所介绍的原则进行相应的转换
(2)二进制位的最高位是符号位,"1"表示负,"0"表示正
(3)对于负数补码而言根据上面所介绍的内容逆推即可得到负数的原码

//比如在32为机器下:
int a=10;
10---00000000000000000000000000001010(原码)
  ---00000000000000000000000000001010(反码)
  ---00000000000000000000000000001010(补码)

int a=-10;
-10---10000000000000000000000000001010(原码)
   ---11111111111111111111111111110101(反码)
   ---11111111111111111111111111110110(补码)

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

为什么呢?

👇👇👇
在计算机系统当中,数值一律用补码来表示和存储的。因为,使用补码,可以将符号位和值域统一处理;同时,如果你了解CPU的话,会知道,在CPU内部只存在加法器,所以补码的优势就更加明显了。此外,补码和原码可以互相转换,运算过程是相同的,不需要其他额外的硬件电路。我们既可以对原码取反加1得到补码,也可以对补码取反加1得到原码。

我们接下来看一个例子:

在这里插入图片描述

int main()
{
	int a = 20;
	int b = -10;
}
20——00000000000000000000000000010100(原码、反码、补码)
-10——10000000000000000000000000001010(原码)
   ——11111111111111111111111111110101(反码)
   ——11111111111111111111111111110110(补码)

前面提到,int是4个字节,而每4个二进制位代表一个16进制位,所以20的补码的16进制表示是00000014,-10的补码的16进制表示是fffffff6,对照上面图片从而可以验证整形数据以补码形式存储这个说法。

对比后,大家其实可以发现我们写出的16进制与图片中内存显示的字节顺序是相反的,那为什么会产生这种情况呢?接下来就介绍产生这种逆序的原因。

💨3.2大端、小端

先引入大小端概念:
大端存储方式:指数据的低位保存在高地址中,而数据的高位保存在低地址中。
小端存储方式:指数据的低位保存在低地址中,数据的高位保存在高地址中。

而上一小节例子中,图片显示的就是小端存储,所以X86环境中结构就是小端存储方式,那么如何通过编程分析某系统环境是大端存储还是小端存储呢❓

#include<stdio.h>
int main()
{
	int a = 1;
	char* p = (char*)&a;
	if (*p == 1)
	{
		printf("小端存储!\n");
	}
	else
	{
		printf("大端存储!\n");
	}
	return 0;
}

代码分析:
此代码是在X86环境下实现的,因为a=1,而int型占4个字节,将其地址强制类型转换成char类型给指针p,此时指针可以访问a的起始地址的一个字节,而1的补码是000000000000000000000000000000001,取8位,换算成10进制后,如果其第一个字节的值为1的话,则可验证它是小端存储。

🟢4.整型提升和算数转换

💨4.1整型提升

我们先引入什么是整型提升?
定义:C语言的整形运算总是以默认精度来进行的,为了获得这个精度,表达式中的字符和短整型操作数在使用之前总是被转换为普通类型,这种转换被称为整形提升。

换句话说,当计算时表达式中如果存在小于int类型的整型值,都必须先转换成int型或者unsigned int型然后去执行运算。

看几个例子:
1️⃣

char a,b,c;
c=a+b;//a和b先进行整型提升相加赋给c后,结果被截断存于c中

2️⃣

char c1=-1;
char c2=1;
//因为char类型带有符号,所以整型提升时,高位补符号位
所以-1整型提升后为:
11111111111111111111111111111111
1整型提升后为:
00000000000000000000000000000001

3️⃣

unsigned char c=1;
无符号数整型提升高位补0

➡整型提升是针对于补码而言的,所以运算时应先转换为补码在进行计算。

💨4.2算术转换

在这里插入图片描述

如果两个操作数进行运算,某个操作数的类型排名较低,那么首先要转换成另外一个操作数的类型进行运算。

🔵5.例题

在了解到整形数据的存储及其相应的转换后,接下来介绍整型如何进行运算!

💨5.11️⃣

#include<stdio.h>
int main()
{
	char a = -1;
	signed char b = -1;
	unsigned char c = -1;
	printf("%d %d %d", a, b, c);
	return 0;
}
输出什么?

代码分析:因为打印%d的数据,即有符号的整数,所以需要进行整型提升,对char类型的a和b进行提升,之后打印原码,所以a=b=-1;而unsigned int的取值范围是0~255,而将-1赋给c后,因为-1的补码是11111111111111111111111111111111,截取1个字节即8位后赋值给c,此时c存储的是11111111为255,因为c为无符号数,所以整型提升高位补0:00000000000000000000000011111111,所以打印出来c的值是255

💨5.22️⃣

#include<stdio.h>
int main()
{
	char a = -128;
	printf("%u", a);
	return 0;
}
输出什么?
10000000000000000000000010000000(原码)
11111111111111111111111101111111(反码)
11111111111111111111111110000000(补码)

代码分析:
因为char占一个字节,截取后为10000000,因为打印无符号整数,所以先进行整型提升,因为a是char类型为带符号类型,所以高位补符号位,将截取后的最高位当成符号位,即整型提升后为:11111111111111111111111110000000,而打印无符号整数,系统认为最高位此时不属于符号位,变为权值位,所以输出结果为4294967168

💨5.33️⃣

#include<stdio.h>
int main()
{
	char a = 128; 
	printf("%u", a);
	return 0;
a的原码、反码、补码相同:
00000000000000000000000010000000
}

128的补码在上面已列出,而在存储的时候需要截断成8位则:10000000,而打印无符号整数,则先进行整型提升,提升时因为a是char类型,是带有符号的,将截断后的二进制位的最高位作为符号位,所以提升后为:11111111111111111111111110000000,而打印无符号数,系统认为最高位不是符号位,所以仍然输出4294967168

💨5.44️⃣

#include<stdio.h>
int main()
{   unsigned int i;
	for (i = 9; i >= 0; i--)
	{
		printf("%u", i);
	}
	return 0;
}

代码分析:i是无符号数,范围是(0,2^32-1),在打印完9、8、7、6、5、4、3、2、1、0后,i变成-1。前面提到,i的补码是11111111111111111111111111111111,而定义时将i定义为无符号数,所以系统默认为11111111111111111111111111111111是一个无符号整数,转换成10进制是4294967295,大于0,所以是个死循环。

💨5.55️⃣

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

代码分析:
先看下图👇
在这里插入图片描述
上图所示,顺时针时我们不断加1,值会从0逐渐变到-128乃至-1,反之,逆时针-1是,则会从-1变到-128之后变为127乃至0。

我们知道,char类型取值范围是(-128,127),所以此代码在进行for循环时,给-1减值不断赋给数组a。而字符’\0’作为字符串结束的标志对应于数值0,在循环赋值时,则-1,-2…-128,127,126…2,1,0;与上面解释刚好对应!

💨5.66️⃣

#include<stdio.h>
int main()
{
	if (strlen("abc") - strlen("abcdef") > 0)
	{
		printf(">\n");
	}
	else
	{
		printf("<\n");
	}
}

我在之前博客中提到,strlen()是计算字符串长度的库函数,那这个代码输出什么呢?
“abc”——3,“abcdef”——6,3-6= -3,所以是<号,对吗?
strlen()的返回值是无符号数,所以3和6都是无符号数,无符号数相减时得到的-3也应该是无符号数,我们不妨写出-3的补码:11111111111111111111111111111101,系统认为此时这个二进制码就代表了一个无符号数,不难发现,转换成10进制后是一个很大的正整数。所以势必是大于0的,所以答案输出是">"。

🟣6.浮点数在内存中的存储

💨6.1

在介绍本节内容前,我们先看一个例子:

#include<stdio.h>
int main()
{
	int n = 9;
	float* p = (float*)&n;
	printf("n=%d\n", n);
	printf("*p=%f\n", *p);
	*p = 9.0;
	printf("n=%d\n", n);
	printf("*p=%f\n", *p);
	return 0;
}

在这里插入图片描述
运行结果好像不是我们预期的,接下来我们先看浮点数如何存储,在转回来解释此现象。

💨6.2

浮点数的存储的国际标准,任意一个浮点数都可以表示成下面的形式:

  • (-1)^ S * M * 2 ^ E
  • (-1)^ S 表示符号位,S=0时为正数,S=1时为负数
  • M表示有效数字,大于等于1小于2
  • 2^E表示指数位

举例来说:
十进制的5.0,写成二进制是101.0,相当于1.01*2^2,其中S=0,M=1.01,E=2。

IEEE 754规定:
对于32位浮点数,最高1位符号位S,接着8位是指数E,剩下的23位是有效数字M。

在这里插入图片描述

同理对于64位浮点数,最高1位是符号位S,接着11位是指数E,剩下的52位是有效数字M。

对于S比较简单不进行赘述,计算机在保存M时,默认这个数的第一个位总是1,所以可以省去,只保留后面小数部分。例如,在保存1.01时,只保存01给M即可!

我们着重讨论指数E:
1.指数E的存入:

首先,E是一个无符号整数。而科学计数法中E可以是负数,所以就需要进行修正。如果是32位时,E是8位,取值范围是0-255,此时E+127,如果是64位时,E是11位,范围是0-2047,此时E+1023。

2.浮点数从内存中取出:

(1)E不全为0或1时:对E减去127或1023,得到真实值E,再将有效数字的M前加上第一位的1;
(2)E全为1时:如果有效数字M全为0,表示无穷大;
(3)E全为0时:此时M不用加上第一位的1,E的真实值为1-127或者1-1023,此时表示±0或者接近0的数。

前述代码分析:
首先,打印n=9毋庸置疑。在利用指针打印n时,由于指针是float类型,n也转换成float类型,所以打印时按照浮点数存取规则进行打印:

0 00000000 00000000000000000001001
S=0;
E=00000000;
M=00000000000000000001001;
与上面介绍的E=0时情况相同表示0,所以很好的验证了上述结果

而当n=9.0后,此时将一个浮点数存入内存当中,按照浮点数存储方式进行存储。

9.0-->1001.0-->(-1)^0*1.001*2^3
S=0,E=3,M=1.001
对E进行修正,结果是130
所以其二进制表示为:
0 10000010 00100000000000000000000
转换成十进制是1091567616

🟤7.Ending

关于浮点数和整型数据的存储也全部介绍完了,关于浮点数的存储是比较麻烦的。整型的各种有符号以及无符号数使用时也应该注意,看清楚其存储范围,合理应用。

这篇博客零零碎碎写了不短的时间,后期博客中的代码我寻思在B站上发些视频,本次的内容就到这里了,点个赞加个收藏再走呗铁子!
在这里插入图片描述

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kkkkvvvvvxxx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值