数据在内存中的存储

在这里插入图片描述

**但行前路,不负韶华!**

本文将十分直白地讲解C语言数据在内存中的存储,让大家更加了解不同数据在内存中是如何被存储的,详解整型提升,同时解开浮点型在内存如何存储的疑惑,还会穿插一些题目来加深大家的印象。

文章目录

      • 原码,反码,补码
      • 整型提升
      • 大小端介绍
      • 浮点型在内存中的存储

前言
不同类型的数据在内存中开辟的空间不同。
数据在内存中的存储_第1张图片
他们在内存中是如何存储的呢?


原码,反码,补码

计算机有三种2进制表示方法,都有符号位和数值位两个部分,

  • 符号位用‘0’表示整数,用‘1’表示负数
  • 正数的原反补码都相同
  • 负数的原反补码各不相同
  • 原码:直接将该数按照二进制的方式翻译过来就是原码。符号位为1。
  • 反码:符号位不变,其他位置按位取反即为反码
  • 补码:反码加1得到的数就是补码。

 要记住的是:对于整形数据(包括字符类型数据)来说,内存中存放的是补码
如图
数据在内存中的存储_第2张图片
 在计算机系统中,使用补码会方便很多,可以将符号位和数值域统一处理,加法减法也可以统一处理(因为CPU只有加法器)。
如果想进行10减2的运算,但计算机之中只有加法器,转化为10加上(-2)进行计算
数据在内存中的存储_第3张图片
符号位进的1超出整型的范围,就被截断了,这样就可以将符号位和数值位进行统一的计算。


整型提升

什么是整型提升呢?

C语言的整型运算总是至少以缺省类型的精度来进行的,为了获取这个精度,表达式中的短整型和字符类型的数据在进行计算时,通常会提升到普通整形。说白了就是将两个短整型数据运算时以整型数据来进行运算,运算完成后得到的结果将发生截断。

整型提升的规则

  • 有符号:按照补码被截断的最高位进行提升,如果最高位是1,就在前边补1,直到变成32位的int类型。
  • 无符号:直接补零,到32位的int类型
    举一个例子
int main()
{
	char a, b, c;
	a = 127;
	//补码为0111 1111
	b = 5;
	//补码为0000 0101
	c = a + b;
	//在进行运算时进行整型提升
	//a提升为0000 0000 0000 0000 0000 0000 0111 1111
	//b提升为0000 0000 0000 0000 0000 0000 0000 0101
	//相加后 0000 0000 0000 0000 0000 0000 1000 0100
	//因为c也是char类型,必将发生截断
	//1000 0100,前边的位置补1
	//补码1111 1111 1111 1111 1111 1111 1000 0100
	//转化为原码,补码减1取反,符号位不变
	//1000 0000 0000 0000 0000 0000 0111 1100
	
	printf("%d", c);
	//以%d形式打印,结果为-124.
	return 0;
}

注:整型提升和普通的算术类型转换不一样,类型转换是因为两个或多个不同类型的数据进行计算时,将每个数据都转化为同一类型即范围最大的类型,而整形提升尽管计算的数据类型相同,还是可能会发生整型提升。

一个很直观的梨子:
数据在内存中的存储_第4张图片
a+b是一个算术表达式,a和b都提升至int型,所以占4个字节。
数据在内存中的存储_第5张图片
c为a+b的返回值,发生截断,变回char类型,所以只占一个字节。
 short类型或char类型运算时,会提升至int类型,要将计算结果放在一个char类型或short类型的变量时,发生截断。


  • 来看一题
int main()
{
	char a = -1;
	signed char b = -1;
	unsigned char c = -1;
	printf("%d %d %d", a, b, c);
	return 0;
}

运行结果如何呢?

-1的补码为1111 1111以%d形式打印,原码为补码减1取反,结果为-1,所以a和b的结果相同,皆为-1,而c为无符号类型,原反补相同,就算进行提升,前位也补0,11111111的十进制为255,故c的打印结果为255.


  • 再看一题
int main()
{
	char a = -128;
	printf("%u\n", a);
	return 0;
}

运行结果如何?
 这里以%u的方式打印,-128在内存中的补码为1111 1111 1111 1111 1111 1111 1000 0000,a是char类型,所以a的补码为1000 0000。%u的意思是以十进制的方式打印无符号整型。在打印1000 0000时必将发生整型提升,整型提升时前边补零或是补一看的是符号位,所以前位补1。
提升后的补码为1111 1111 1111 1111 1111 1111 1000 0000,正数的原反补都相同,换成十进制
数据在内存中的存储_第6张图片
放在VS里运行后结果确实如此
数据在内存中的存储_第7张图片

int main()
{
	char a = 128;
	printf("%u\n", a);
	return 0;
}

道理相同,128的补码为0000 0000 0000 0000 0000 0000 1000 0000,a的补码为1000 0000,他和a是-128时存进内存的补码一样,a的类型同样为有符号类型,整型提升前位补1,所以打印结果一模一样。


大小端介绍

大端小端是两种存储的方式,大端储存方式是数据的低位保存到内存的高地址中,数据的高位保存在内存的低地址处,小端存储方式是指数据的地位保存在低地址中,而数据的高位保存在内存的高地址处。

char类型的数据,在内存中只有一个字节,就不存在大小端问题,然而除了一个字节的char字符类型外还有2个字节的short,4个字节的int,等等等等,对于位数大于八位的处理器,由于寄存器的宽度大于一个字节,那么必然会出现如何对多个字节进行安排的问题,这就分化出了大小端问题。

如图详细介绍一下大小端
数据在内存中的存储_第8张图片
那么如何知道一个编译器具体是大端还是小端?
这是百度在2015年工程师笔试题里的问题,设计一个小程序判断当前机器的字节序是大端还是小端。
大端的话低位放在高地址,高位放在低地址。小端的话相反。
直接写一个代码调试先看一看
数据在内存中的存储_第9张图片
将右上角的列改为1,地址输入取地址a
在这里插入图片描述
可以看到低地址储存的是低位,高地址存的是高位。

  • 写一个小程序来判断
int main()
{
	int a = 1;
	char* p = (char*)&a;
	if (*p == 1)
	{
		printf("小端\n");
	}
	else
		printf("大端\n");
	return 0;
}

利用强制类型转换,取地址a取出首地址,即四个字节中的低地址位,强制类型转换为字符类型的指针,解引用后从内容即可判断其为0还是1从而判断大小端。

  • 注意不能直接截断来判断
    例如int b=(char)a;截断从低位开始,如果是很大的数截断会把高位截断,而保存低位。所以用这种方式明显是不行的。

浮点型在内存中的存储

常见的浮点数类型有float,double,long double类型。
浮点数分为整数部分和小数部分,他们在内存中的存储和整型有什么区别?
看代码

int main()
{
	int n = 9;
	float* pfloat = (float*)&n;
	printf("n的值为:%d\n", n);
	printf("*pfloat的值为:%f\n", *pfloat);

	*pfloat = 9.0;
	printf("n的值为:%d\n", n);
	printf("*pfloat的值为:%f", *pfloat);

	return 0;
}

运行后结果如图
在这里插入图片描述
num和*pfloat在内存中明明是同一个数,为什么结果差别如此之大,说明了浮点数和整形数据在内存中的储存方式不同。
根据国际标准,任何一个二进制浮点数V都可以用以下方式表示

(-1)^S *M *2^E (-1)^S表示符号位,如果S为0,V为正数,S为1,V为负数。
M表示有效数据,大于等于1,小于2。
2^E表示指数位。

举例
十进制的5.0,二进制形式为101.0,相当于1.01*2^2。用二进制浮点数表示(-1) ^0 * 1.01 * 2 ^2。
在这里S为0,M为1.01,E为2。
类比于十进制123,相当于1.23 * 10^2。
例如
十进制的浮点数5.5
二进制形式可以写作101.1,相当与1.011 * 2^ 2,用(-1)^0 * 1.011 * 2 * 2。
浮点数0.5
二进制形式0.1,相当于1.0 * 2^(-1)。
根据国际标准IEEE(电气和电子工程协会)规定:

对于32位的浮点数float而言,最高的1位是符号位S,接下来的8位是指数E,剩下的23位为M。

在这里插入图片描述
对于64位的浮点数double,最高的1位仍为S,接下来的11位是指数E,剩下的52位为有效数字M。
在这里插入图片描述
前边已经说过了,M大于1小于2,所以无论什么时候M都可以写作1.XXXXXX的形式,所以在保存M时,只保存1后边的部分,这样可以省出来一位。就比如32位float类型,留给M的只有23位,不保留小数点前的1的话,就可以保存24位有效数字。
对于指数E
首先E为一个无符号整数(unsigned int)

如果E为8位,他的取值范围为0~255;如果E位11位,它的取值范围为0
~2047。但是就像上述所说的0.5,E是可能出现负数的,所由IEEE规定,存入内存时,E的真实值必须要加上一个中间数,对于8位的E,这个中间数是127,对于11位E,这个中间数是1023。

比如2^10的E为10,在保存为32位的浮点数时,在内存中保存的数是10+127=137。即10001001。


  • 指数E从内存中取出还分为三种情况
  1. 指数E不全0或不全为1。

(按照32位的float来说)取出直接指数E的值直接减去127。得到E的真实值后,将M前边的1补上,再根据S是0还是1,判断这个数的正负。
例如前边所说的0.5,二进制形式为0.1,正数部分必须为1,小数点前进一位。变为1.0*2^(-1)。
E的值为(-1)+127为126,表示为01111110。1.0去除1,只留下0,补齐至23位,就是23个零。而且因为他是正数。S为0.
在内存中为0 0111 1110 0000 0000 0000 0000 0000 000
2. 当E为全0的情况

如果E为全零,那他原来的值就是-127。那将是一个超级小的数字,2的10次方就是1024,2的-127次方接近于无穷小,这时直接判定E的值为1-127。有效数字M不再加上第一位的1,而是还原为0.XXXXX的小数,这样做是为了表示±0,以及接近于0的很小的数字。
3. 当E全为1

如果E全为1,那么在没有加上中间值时,E得值为128,2的128次幂是一个很大很大的数,如果有效数字M全部为零,就表示无穷大,是正无穷还是负无穷取决于S。


现在再来解决上边的疑问。

int n = 9;
float* pfloat = (float*)&n;
printf(“n的值为:%d\n”, n);
printf(“*pfloat的值为:%f\n”, *pfloat);
*pfloat = 9.0;
printf(“n的值为:%d\n”, n);
printf(“*pfloat的值为:%f”, *pfloat);
return 0;

  • 以%d形式打印n我想就不用说了。
  • 在内存中n=9的二进制为0000 0000 0000 0000 0000 0000 0000 1001

因为pfloat强制类型转化为float类型的指针,就是把该二进制序列看作浮点数的形式用%f的形式打印。
0 00000000 00000000000000000001001
用浮点数的储存方法解读,S为0,E为全零,是一个很小很小的数,而%f打印默认只能保留到小数点后六位。故打印结果为0.000000。

  • *pfloat=9.0。

这个时候就变为了浮点数的形式储存9.0。
9.0的浮点数形式储存,S等于0,1.001 *2^3,E加上127,即130,二进制形式为10000010,M的001,后边全部补0。
综合为 0 10000010 0010 0000 0000 0000 0000 000
数据在内存中的存储_第10张图片
对比上边的十进制结果发现确实如此。

  • 最后以%f的形式打印浮点数,相当于用浮点数的储存方式取出数据,在打印,结果为9.000000。

到了这里一切就都柳暗花明了。本章完成,如果哪里有错误的话还请大家指出。

你可能感兴趣的:(c语言知识详解,c语言,笔记,学习)