深度解析数据在内存中的存储

在C语言中,我们知道常见的数据类型有char、short、int 、long、long long、float、double等,以及它们所占的存储空间大小:数据类型基础知识
而这些类型的意义在于:

  1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
  2. 如何看待内存空间的视角。

下面作具体讲解。

1.类型基本归类

整型家族:

char型:
unsigned char
signed char
short型:
unsigned short
signed short
int型:
unsigned int
signed int
long型:
unsigned long
signed long

补充说明:
1.char型到底是 signed char 还是 unsigned char ?
答:C语言标准并没有规定,取决于编译器
2.我们常用的 int 型就为 signed int 型。
3.在编程中其他类型如没特别说明是unsigned型,都默认为 signed 型。

浮点型家族:

float—单精度浮点型
double—双精度浮点型

那么这些数据怎样在内存中存储的呢?

2.整型在内存中的存储

首先了解如下概念:

2.1. 原码、反码、补码

计算机中的整数有三种表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位位于二进制序列的最高位,用 0 表示“正”,用 1 表示“负”,而数值位中负整数的三种表示方法各不相同,分别如下:

原码:
直接将二进制按照正负数的形式翻译成二进制就可以
反码:
将原码的符号位不变,其他位依次按位取反就可以得到了
补码:
反码+1就得到补码

对于正数来说,正数的原、反、补码都相同。
对于整形来说:无论正数还是负数,数据存放内存中其实存放的都是补码,补码,补码!!!

这是为什么呢?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路(即补码通过+1取反也会得到原码)。
具体看代码:
在这里插入图片描述
从上图红色圈起来的部分可以看出正负数在内存中是以不同的形式存储的;当然,我相信细心的人也看出了黄色方框中数据放入的顺序不一样,这又是为什么呢?

接下来就要介绍一下大小端

2.2. 大小端

什么是大、小端:

大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
在这里插入图片描述

所以若为上图中1就为大端存储,为2就为小端存储。
这里有一道题目:
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。
代码如下:

int check_sys()
{
	int i = 1;
	char* p = &i;//定义为char*类型的指针,目的是为了只访问一个字节
	return *p;//*p=0就返回 0,*p=1就返回 1
}
int main()
{
	int ret = check_sys();
	if (ret == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

在这里插入图片描述
所以经过测试,当前机器为小端字节序。

2.3. 练习

以下题目会涉及整型提升的知识,如有不明白的地方,可查看:
C语言中的整型提升

题目1:
下面程序输出什么?

#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;
}

在这里插入图片描述
解析:

char a= -1;
-1的二进制序列为:10000000000000000000000000000001
转换成补码:11111111111111111111111111111111
变量a为char类型,只有一个字节(即8个比特位),所以将-1放入a中时首先会发生截断,所以a中的二进制序列就为:11111111
signed char b=-1;
变量b的类型为signed char型,所以放入b中的二进制序列与变量a中的一样,为:11111111
unsigned char c=-1;
c为 unsigned char型,-1放进去也会发生截断,即为:11111111
printf(“a=%d,b=%d,c=%d”,a,b,c);
a、b在打印出来之前会根据自己的类型来整型提升,因为a、b都为char型,所以提升为: 11111111111111111111111111111111
而提升后是以补码的形式放入内存中的,转换为原码以‘%d’(有符号整型)打印出就为:1
c为unsigned 型,打印出来之前会以unsigned形式提升,提升为:
00000000000000000000000011111111
所以‘%d’的形式打印出来就为255

题目2:
下面代码输出什么?

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

在这里插入图片描述
解析:

char a = -128;
-128的二进制序列:10000000000000000000000010000000
补码为:11111111111111111111111110000000
a为char型,放入a中发生截断,所以最后放入的序列为:10000000
printf("%u\n",a);
a在打印出来之前会根据自己的类型做整型提升,a 为char型,所以提升为:
11111111111111111111111110000000
以‘%u’(无符号(unsigned)整型)形式打印出来就为 4292967168

题目3:

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

在这里插入图片描述
解析:

此题与题目2存在的不同一点就在于a为正数,128放入a中前会发生截断,截断后放入的二进制序列为:10000000
最后打印的过程同题目2一样。

补充1(算数转换)

在以下题目前补充算数转换相关知识:
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。(以下类型依次至下而上转换)


在这里插入图片描述
例如:计算a+b,其中a为int 型,b为unsigned int型,则在计算时会将a转换为与a一致的类型,即unsigned int 型。

题目4:

int i= -20;
unsigned  int  j = 10;
printf("%d\n", i+j); 

在这里插入图片描述
解析:

i、j 的类型不同,所以在进行相加运算时会发生算数转换,然后按照补码的形式进行运算,最后打印格式为有符号整数(‘%d’形式)
i 的补码:11111111111111111111111111101100
j 的补码(原码):00000000000000000000000000001010
相加后为:11111111111111111111111111110110(补码)
原码为:10000000000000000000000000001010
所以打印出为 -10

补充2(char类型变量范围)

在程序运行过程中有些情况下会涉及到各种类型的范围,这里以char类型为例,来讲解变量的范围
在这里插入图片描述
所以char类型变量的范围为 -128 ~ 127, 为了加深理解,下面一张图能很好的说明char类型范围中数值的变化
在这里插入图片描述
所以可以看出一点,在char类型中,127+1 后会变为 -128
延伸:
unsigned char 类型变量的取值范围就为 0 ~ 255
同char类型一致,int类型变量的范围为 -2^31 ~ 2^31-1
unsigned int 变量的范围为 0 ~ 2^32-1

题目5:

unsigned int i;
for(i = 9; i >= 0; i--)
{
    printf("%u\n",i);
}

答案:9 8 7 6 . . . 死循环下去
解析:

因为变量 i 为unsigned int 型,所以 i 的范围为 0 ~ 2^32-1,始终不会小于0,循环就会一直运行下去,最终死循环

题目6:

#include <stdio.h>
unsigned char i = 0;
int main()
{
    for(i = 0;i<=255;i++)
   {
        printf("hello world\n");
   }
    return 0;
}

在这里插入图片描述
解析:

i 为unsigned char型,所以i 会始终 <= 255,程序会一直死循环下去

题目7:

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

在这里插入图片描述
解析:

a为char型,所以a变量的范围为 -128 ~ 127
随着循环的运行,a[i]变化情况为:
-1 -2 -3 . . . -127 -128 127 126 . . . 0 -1 -2 . . .
strlen函数计算的是‘\0’(0)前的字符个数,所以循环到第一次a[i]=0,时就会停止,此时0前面的数字共有255个,所以打印为255

3.浮点型在内存中的存储

常见的浮点数例如:3.14159、1E5(表示1.0x10^5次方)
浮点数家族包括: float、double、long double 类型。
浮点数表示的范围:float.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;
}

在这里插入图片描述
可以看出,不同的数据类型以不同的方式打印出来的结果也各不相同。

3.1. 浮点数存储规则

num 和 *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,就一定要弄懂浮点数在计算机内部的表示方法。
这里就需要理解IEEE(电气和电子工程协会) 754标准.

IEEE754标准

根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数v可以表示成下面的形式:

即 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 。
那么,按照上面 V 的格式,可以得出 S=0,M=1.01,E=2。
十进制的 -5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,
S=1,M=1.01,E=2。

那么浮点数具体是怎样存储的呢?(存储方式)

IEEE754规定:
对于32位的浮点数,最高的1位是符号位 S ,接着的8位是指数 E ,剩下的23位为有效数字 M
在这里插入图片描述
对于64位的浮点数,最高的1位是符号位 s,接着的11位是指数 E,剩下的52位为有效数字 M
在这里插入图片描述
IEEE 754对有效数字M和指数E,还有一些特别规定

前面说过, 1≤M<2 ,也就是说,M 可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。
IEEE 754规定,在计算机内部保存 M 时,默认这个数的第一位总是 1 ,因此可以被舍去,只保存后面的 xxxxxx 部分。比如保存1.01的时候,只保存 01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省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。

然后,指数 E 从内存中取出还可以再分成三种情况:(取出方式)

1)E 不全为 0 或不全为 1(常见情况)

这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
比如:
0.5(1/2)的二进制形式为 0.1,由于规定正数部分必须为1,即将小数点右移1位,则为 1.0*2^(-1) ,其阶码为 -1+127=126 ,表示为 01111110 ,而尾数 1.0 去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制表示形式为:
在这里插入图片描述

2)E全为0

这时,浮点数的指数的真实值等于 -127(或者 -1023 ),有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。即当E全为0时,这个数等于0或者无限接近0。

3)E全为1

这时,意味着浮点数指数的真实值为128(255-127),如果有效数字M全为0,表示±无穷大(正负取决于符号位s)

OK,关于浮点数的表示规则,就说到这里。

再次回到前面的例子

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

n 本身就是 int 型,当以‘%d’形式打印出来时不会变化,仍为 9

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

*pFloat=9,即以整型的形式存入,而已‘ %f ’的形式取出(打印出来),
所以取出时是以浮点型的视角看待二进制序列的,视角如下
在这里插入图片描述
由于指数E全为0,所以符合上一节的第二种情况。因此,浮点数V就写成:
V = (-1)^0 × 0.00000000000000000001001×2^(-126) =1.001×2^(-146)
显然,V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000

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

将9.0放入 *pFloat中,即以浮点型方式将9.0存入n中 ,又以‘%d’(整型)形式取出,方式为:
首先,浮点数9.0等于二进制的1001.0,即1.001×2^3。
而 9.0 -> 1001.0 -> (-1)^0 x 1.001 x 2 ^3 -> S=0,M=1.001,E=3+127=130
那么,第一位的符号位 S=0,有效数字 M 等于001后面再加20个0,凑满23位,指数 E 等于3+127=130,即10000010。所以,写成二进制形式,应该是S+E+M,即
在这里插入图片描述
这个32位的二进制数,还原成十进制,正是 1091567616

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

将9.0放入 *pFloat中,即以浮点型的形式将数据存入内存中,而已‘%f’的形式将数据取出,因为存入与取出的形式一致,所以打印出来为9.000000

以上就是对数据在内中存储的解析。

  • 21
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值