数据在内存中的存储


前言

本文主要介绍整数、浮点数在内存中的存储


1. 整数在内存中的存储

整型有无符号整型(unsigned int)和有符号整型(int)两种,有符号整型分正负,二进制有符号位和数值位之分。

1.1 原码 反码 补码

整数的二进制表示方法有三种,即原码、反码和补码,有符号的整数,三种表示方法均有符号位数值位两部分,符号位都是用0表示“”,用1表示“”,最高位的一位被当做符号位,剩余的都是数值位。
正整数的原码、反码、补码都相同,负整数的三种表示方法之间的关系如下图所示

在这里插入图片描述
原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码。
原码转换为补码的方式符号位不变,其他位按位取反,+1与补码到原码路径1的方式一致

对于整型来说,数据在内存中以补码存放。

在计算机系统中,数值一律用补码来表示和存储。
使用补码,可以将符号位和数值域统一处理,同时,加法和减法也可以统一处理(CPU只有加法处理器)。此外,补码与原码相互转换,其运算过程也是相同的,不需要额外的硬件电路。

1.2 大小端字节序

在看大小端字节序之前,我们先看一个例子,如下图所示,
在这里插入图片描述

初始化整数a=0x11223344,我们通过调试,在内存窗口看到a在内存中的存储为44 33 22 11(数据在内存中是以二进制存储的,但为了方便,在这里是以16进制的形式展示),并且可以观察到地址是由低到高的,如下图所示,这也就是小端存储模式。

在这里插入图片描述

大小端字节序是针对整数在内存中的存储而言的,

1.2.1 什么是大小端字节序

其实,超过一个字节的数据在内存中存储就会存在字节间存储顺序的问题,地址都有各自的编号,从小到大变化,也就是地址从低到高变化,那么根据数据低位字节内容与高位字节内容存储的顺序不同,我们可以将其分为大端和小端两种模式。
具体概念
大端存储模式

是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容保存在内存的低地址处。

小端存储模式

是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容保存在内存的高地址处。

如下图所示
在这里插入图片描述

1.2.2 为什么会有大小端之分

其一,在计算机系统中,以字节为单位,每一个单位对应一个字节,一个字节为八个比特位,但在C语言中除了8bits的char之外,还有16bit的short型,32bit的long型(要看具体的编译器);
其二,对于位数大于8的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题,
因此导致了大端存储模式和小端存储模式。

例如:一个16bitshortx,在内存中的地址为0x0010x的值为0x1122,那么,0x11为高字节,0x22为低字节。对于大端模式,将0x11放在低地址0x0010中,将0x22放在高地址0x0011中,小端模式则正好相反。我们常用的X86结构是小端模式,而KEIL C51是大端模式。很多的ARM,DSP都为小端模式。有的ARM处理器还可以由硬件来选择是大端模式还是小端模式。

1.2.3 大小端字节序的判断

百度笔试题(10分)
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。

如果是大端存储,int是4个字节,1在内存中的是0x00 00 00 01,如果是小端字节序,01存储在低地址,返回结果是1;如果是大端字节序,返回结果是0,代码如下所示

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

还有另外一种算法,这个要结合联合体(union)的知识,联合体是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型,但每次是能使用其中的一个成员,下面的联合体un包含两个成员,int i和char c,共享同一块内存空间。将1的值赋给i,如果小端字节序,最低位存储的是01,调用c时返回的是1,如果是大端,返回0.

int check_sys2()
{
	union
	{
		int i;
		char c;
	}un;
	un.i = 1;
	return un.c;
}

1.2.4 相关题目及解析

下面我们看一写题目,一定要注意各个类型如char类型的数据能存储数据的范围!不然,每个答案都会大吃一惊~

1.2.4.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;
}

VS2022 x86环境测试结果如下

a=-1,b=-1,c=255

-1在内存中的二进制存储如下图所示,
char具体是unsigned char还是signed char,取决于编译器,但大多数情况下都是signed chaer,下文均以signed char对char进行讨论。
因为char只有8bit,只能存一个字节,因此要对整数-1的补码进行截断,从低位开始存,a, b的补码均为11111111,当以%d也就是整数的方式进行打印,要进行整型提升,整形提升分有符号数与无符号数,如果是有符号数,高位补符号位;如果是无符号数,高位补0.
对于a,b,是有符号的数,最高位1是符号位,高位补符号位,那么打印时a, b的补码被整型提升为11111111 11111111 11111111 11111111,对应原码表示-1;
对于c,是无符号数,高位补0,c提升后的补码为00000000 00000000 00000000 11111111,对应数值为255.

在这里插入图片描述

1.2.4.2 题目二

代码1

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

代码2

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

VS2022 x86环境测试结果如下

4294967168
4294967168

代码1分析

-128的原码、反码、补码如下
在这里插入图片描述
a只能存10000000,整型提升后11111111 11111111 11111111 10000000,以%u的方式打印,也就是11111111 11111111 11111111 10000000对应的正整数,结果是4294967168.

代码2分析

128的原码、反码、补码如下
在这里插入图片描述
a存10000000,与上述问题中a的补码一致,且都是有符号数,以%u打印,因此结果一致。

补充:
下图是char在内存中的二进制转换,char类型数值范围是-128~127,也就是说存不下128.
在这里插入图片描述

1.2.4.3 题目三
#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;
}

VS2022 x86环境测试结果如下

255

这个要结合我们上面的图,streln统计的是’\0’之前出现的字符个数,'\0’的ASCII码值是0,在上图,我们看到顺时针是+1,逆时针也就是-1;
那么a[i]的值就是-1, -2, -3, … , -127, -128, 127, 126, … , 2, 1, 0, -1(循环,如下图所示)
在这里插入图片描述
当第一次出现0,strlen停止计数,-1~-127是128个数,127 ~1是127个数,总共是255.

1.2.4.4 题目四

代码1

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

代码2

#include <stdio.h>
int main()
{
    unsigned int i;

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

我们可以看到VS已经对错误进行了提示
在这里插入图片描述
在这里插入图片描述
代码1分析
与char类似,ungigned char取值范围是0~255,如下图所示
第一个代码i从0依次递增1到255,+1得到0,循环往复,因此是死循环打印hello world.

在这里插入图片描述
如下图,我们让系统打印完休眠30ms,可以看到i的变化。
在这里插入图片描述

代码2分析
此时i是int型,我们可以看到i的值9 8 7 … 0 4294967295,这是因为0-1=-1,-1对应补码是11111111 11111111 11111111 11111111,以%u打印,原码反码补码相同,打印结果是4294967295.整数和char类型的加减类似,不过int是四个字节,存储最大的数是4294967295.
如下图所示
在这里插入图片描述
unsigned int如下图所示
在这里插入图片描述

1.2.4.5 题目五
#include <stdio.h>
//X86环境 ⼩端字节序
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;
}

VS2022 x86环境测试结果如下

4,2000000

如下图所示,&a取出的是整个数组的地址,+1跳过整个数组,ptr1指向4后面的位置,(int*)强制类型转换ptr1,ptr1[-1]等价于*(ptr1-1),即ptr1-1指向4,解引用访问4.
对于ptr2,(int)a,a表示数组首元素的地址,(int)强制类型转换后+1,整数增加1,(int *)强制类型转换为int *的指针,从a到 (int *)((int)a + 1),跳过1个字节,我们知道X86是小端字节序,低位在低地址,那么数组中0102在内存中的存储是01 00 00 00 02 00 00 00,即此时ptr2指向01后面的第一个00,接着解引用访问四个字节,00 00 00 02,即02是最高位,那么结果就是02000000,如下图所示在这里插入图片描述


2. 浮点数在内存中的存储

先看例子

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

VS2022 x86环境测试结果如下
在这里插入图片描述
可以看到,int型数据以%d打印,float型数据以%f打印,结果和存入的一致;但当int型数据的地址被强制类型转换为float*且解引用得到的值与存入的不一致,float型数据的地址被强制类型转换为int *且解引用得到的值与存入的不一致。
这是因为int型数据和float型数据在内存中的存储方式不一致。

下面我们探讨float型数据在内存中的存储方式
根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式
V=(-1)S *M *2E

  1. (-1)S 表示符号位,当S=0,V是整数;当S=1,V是负数;
  2. M表示有效数字,1=<M<2;
  3. 2E表示指数位。

举个例子,10进制的5.0写成二进制是101.0,相当于1.01*2^2,S=0,M=1.01,E=2.
十进制的5.0,写成二进制是-101.0,相当于-1.01 *2^2,S=-1, M=1.01, E=2

IEEE 754规定:
对于32位的浮点数,最高的1位存储符号S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数,最高的1位存储符号S,接着的11位存储指数E,剩下的52位存储有效数字M
如下图所示
在这里插入图片描述

在这里插入图片描述

2.1 浮点数存的过程

IEEE 754对有效数字M和指数E,还有一些特别规定。
前面说过,1<=M<2,也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。
IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的是节省一位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。

至于指数E,情况就比较复杂。
首先,E为一个无符号整数(unsigned int)
这意味着,如果E为八位,取值范围是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.

2.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,则其二进制表示形式为1 01111110 00000000000000000000000.
E全为0
这时,规定浮点数的指数E等于1-127(或者1-1023),且M不再补1,结果为0.xxxxxx *2^(-126)是一个无穷小的数。
E全为0
这时是一个无穷大的数。

还有可能出现的问题是,有的数据不能在内存中精确存储,会有数据丢失。因为没有办法以二进制的形式将其准确的存储到内存中。

了解浮点数的存储后,我们看一个例子,5.5f,101.1,1.011*2^2,
S=0, E=2+127=129,
0 10000001 01100000000000000000000
40 b0 00 00,小段字节序存储,在内存中为 00 00 b0 40,如下图所示

在这里插入图片描述

2.3 题目解析

在这里插入图片描述
我们再来看刚开始的例子,int n=9在内存中的存储为 00000000 00000000 00000000 00001001,当以%f打印,S=0, E=00000000, 即E全为0,结果是无穷小,输出0.

float 9.0,1.001*2^3,S=0, E=3+127, 对应二进制补码为10000010,M=00100000000000000000000,即01000001 00010000 00000000 00000000,int型打印读取补码对应的值为1091567616.


总结

今天粗略说了数据在内存中的存储,包括整数和浮点数,整数涉及到原码、反码、补码,大小端字节序,以及相关题目;浮点数介绍了IEEE 754规则。
为的是以后如果我们在编程的过程中出现一些我们不期望出现的数字,我们有一个方向可以去考虑,好啦,今天就到这里啦!
愿你三冬暖 愿你天不寒 愿你天黑有灯 下雨有伞 愿你善其身 愿你遇良人 暖色浮余生 有好人相伴 ——公子向北走

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值