深入理解《有理数》在电脑中是怎么存储的

❤️ :热爱编程学习,期待一起交流。
?:博主是河牧院大一在读学生,水平有限,如发现错误,期待指点!(2466200050)
?:以下是我对数据在内存中的存储的拙见,期待大佬们指教。

前言

  • 这篇博客结合代码和理论知识数据在内存中的存储做了深入的分析。毕竟只讲理论不实践分析就是耍流氓。

  • 看完这篇你就了解数据在内存中是如何存储的啦。

  • 如果你想更深入了解,强烈推荐计算机专业必看的一本书《深入理解计算机系统》
    在这里插入图片描述

  • 这本书的第二章的信息存储讲的就是我这篇博客要叙述的。

  • 对这本书感兴趣的可以联系我给你发个电子版的。

  • 虽然不看这本书也能写代码。但是看了之后就有种被打通任督二脉的感觉。因为那时候已经了解他的本质。再次书写代码的时候能够得心应手。

  • 编程三种境界:
    看代码是代码,看代码是内存,看代码还是代码。

    博主正在朝着看代码是内存的道路前进!

  • 从我们初识C语言开始,我们前几课就已经学了各种各样的数据类型。但是我们可能只是了解了这些数据类型怎么去使用,比如这里,int a = 10;我们定义了一个整型变量a,它的值是10。但是我们并不知道这个数字10怎么在内存中存储的。
    在这里插入图片描述

? 数据在内存中以什么形式存储

  • 首先,要设置一个问题,引出接下来要讲的内容。
    以32位机器为例
  • 1.我们定义了一个整型变量a。int a = 10;那么这个10进制的数字10,是怎么在计算机中存储的呢?
  • 2.如果是以二进制的方式存储,那么它是怎么显示的?
  • 大多数计算机使用一个字节(1byte=8bit),作为最小的可寻址的存储器单位,而不是在存储器中访问单独的位(bit)。
  • 接下来,我们讲述编译器和运行时系统是如何将存储器空间划分为更可管理的单元。

?十六进制表示法

  • 一个字节由八个位组成。在二进制表示法中,他的取值范围是00000000~11111111(也就是十进制的0 到 255)
  • 由于二进制表示法太冗长,十进制表示法与位模式的互相转化又很麻烦。
  • 所以诞生了以16进制(HEX)来表示位模式在内存中表示.
  • 它是用0~9以及用字符A到F来表示16个值,也就是表示0到15这16个数字。
  • 在C语言中。以0x或者0X开头的数字就是16进制。字母A到F即可大写也可以小写。

二进制如何转换成16进制

在这里插入图片描述

  • 每四个二进制位可以转换成一个16进制的数字。
  • 例如:给你一个173A4C。如何转换成二进制?
  • 如下所示

在这里插入图片描述这样我们就得到了32位的二进制表示00010111001110101001100

?这个16进制位的数字如何在内存中表示?

大、小端字节序

  • 假设让数字0x11223344放在内存中 ,那他是如何表示的呢?
  • 实际内存中存的就是是它的二进制序列,只是内存中的表示一般是16进制。
  • 我们打开vs,切换到内存。
#include<stdio.h>
int main()
{
	int a = 0x11223344;
	return 0;
}
  • 我们以这段代码为例。我们进入调试。使用&操作符拿到a的地址。然后在内存中的该地址上的数是如何表示。
    在这里插入图片描述
  • 可以看到此时程序刚从主函数入口进入,变量a在内存中已经开辟好了空间。但还没有开始执行下一条int a = 0x11223344的赋值操作。所以a的值是一个随机值1.
  • 我们接着执行
    在这里插入图片描述
  • 这里还没有执行int a = 0x11223344的赋值操作,但是已经为main函数开辟好了栈帧空间。把局部变量都赋值为了cc cc cc cc,所以a这时候还是随机值-858993460。
    在这里插入图片描述
  • 继续执行了下一步赋值操作0x11223344的十进制数就是287454020.
  • 可以看到此时在内存中a的地址为 44 33 22 11
  • 正好是0x11 22 33 44 倒着写过来的。
  • 所以在内存中的存储就是把16进制位按每一个字节(即八个bit位)倒着写上去就行了。
    我们来看看官方怎么描述。

把某些机器选择在存储器上按照从最低有效字节到最高有效字节的顺序存储对象的方法叫做小端法。(我们现在大多数用这种方法)
而另一些机器则按照从最高有效字节到最低有效字节的顺序存储
对象
的方法叫做大端法

特别注意:字节在内存中的存储是从低地址向高地址存储的*

  • 例如: 用小端法存储0x11 22 33 44,11是一个字节,22,33,44都是一个字节(即二进制中的八个bit位)。11是最高字节位,44是最低字节位。按照从最低有效字节到最高有效字节的顺序存储0x11
    22 33 44得到 在内存中表示是44 33 22 11.

关于大小端之争的有趣故事

在这里插入图片描述

  • 由以上的铺垫,这里我们就要步入正题啦

?数据类型的详细介绍

  • 接下来先介绍一下都有什么数据类型,然后我们再对其中的整型类型和浮点类型进行剖析。由于博主水平有限。其他暂时了解即可。

整型类型(剖析)

  • 每个整型可以具体划分为有符号整型和无符号整型。
  • 有符号数的特点
  • 有正负之分。取值范围从负数到正数
  • 二进制位的最高位是符号位。0表示正数,1则表示负数。
  • 无符号数的特点
  • 全是正数,无正负之分。取值范围从0到正数。
  • 二进制位的最高位不是符号位,它的最高位仅用表示该数的大小
  • 32位机器上C语言的整型数据类型的典型取值范围。
    在这里插入图片描述
  • 我们可以看到有unsigned全是正数,就入上面我们所说的那样。
char
 unsigned char//无符号字符类型
 signed char//有符号
short
 unsigned short [int]
 signed short [int]
int
 unsigned int
 signed int
long
 unsigned long [int]
 signed long [int]

浮点数类型(剖析)

float
double

构造类型(了解)

> 数组类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union

指针类型(了解)

int *pi;
char *pc;
float* pf;
void* pv;

空类型(了解)

void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型。

?整型在内存中的存储:原码,反码,补码

  • 我们前面说到一个变量a=0x11223344在内存中是如何存储的了。
  • 但是负数怎么存储呢?
  • 例如:
int a = 10;
int b = -10;
  • 这个int b = -10怎么在内存中存储呢?
  • 我们知道为 b分配四个字节的空间。
    那如何存储?
  • 下面了解下面的概念:
  • 计算机中的整数有三种表示方法,即原码、反码和补码。
  • 三种表示方法均有符号位数值位两部分
  • 符号位都是用0表示“正”,用1表示“负”,而数值位负整数的三种表示方法各不相同。

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

  • 重点:
  • 正数的原、反、补码都相同。
    负数的原、反、补码不相同。
    对于整形来说:数据存放内存中其实存放的是补码。
    为什么呢?
  • 在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统
    一处理;
  • 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
  • 我们拿上面的a = 10和 b = -10 为例,来探究数在内存中是如何存储的。
    在这里插入图片描述
  • 可以看到a的值是00 00 00 0a。a是用10进制表示就是数字10。
  • b的值是ff ff ff fa,转化为10进制就是4294967290,天呐好大的数字!为什么不是-10呢?
  • 让我们来探究一下
00000000 00000000 00000000 00001010 二进制表示的a
11111111 11111111 11111111 11110110 二进制表示的b
  • 对于a ,因为数据在内存中存放的是补码。但a为正数,原,反,补码都相同。所以内存中存的就是10的二进制原码。
00000000 00000000 00000000 00001010 数字10的二进制原码
转化成16进制就是00 00 00 0a
  • 对于b, b是一个负数-10,将-10放进内存中放的是-10的补码。
  • 我们经过对原码符号位不变,其他位按位取反得到反码,反码进行+1,就得到了补码
10000000 00000000 00000000 0000101010的原码
11111111 11111111 11111111 1111010110的反码(符号位不变,其他位按位取反)
11111111 11111111 11111111 11110110 补码 = 反码+1
  • 我们得到了-10的补码,发现和图片上的一样
    在这里插入图片描述
  • 转化成16进制的确是ff ff ff f6。
  • 但为什么打印出来的是-10呢?而不是他的二进制补码对应的数呢
  • 因为打印的话是以原码的形式打印的,但在内存中存储是以
    补码形式存储的

深入理解原码,反码,补码

  • 接下来我们用实例来理解原反补。
  • 下面程序运行的结果是什么?
  • 一共三部曲
#include<stdio.h>
int main()
{
		char a = -1;//默认为有符号
		signed char b = -1;//有符号char类型
		unsigned char c = -1;//无符号char类型
		printf("a=%d,b=%d,c=%d", a, b, c);//a = -1,b = -1,c = 255
		return 0;
}

1.先得到-1原来的原、反、补码

  • 剖析,以下是-1这个整数的(4个字节)原反补码。
10000000 00000000 00000000 00000001 原码
11111111 11111111 11111111 11111110 反码
11111111 11111111 11111111 11111111 补码//(补码就是反码+1)

2.发生截断

  • 我们拿到了-1这个整数的补码。因为内存中存的是补码,现在把4个字节的整型数字负1放进char类型变量a中的话,就要发生截断,只取低一个字节。即11111111(补码)。
11111111 截断后放在a中的补码

3.发生整形提升

  • 我们要以**%d**的形式打印a;必须先整型提升,整型提升看符号位,符号位是1,前面就补够32个1位置。符号位是0,前面就要补够32个0。(一个整型32位4个字节)
11111111 11111111 11111111 11111111 发生整型提升后的补码
    • 因为printf到屏幕上显示出来的是原码
  • 但a中放的是11111111 11111111 11111111 11111111(补码). 所以打印出来的a是10000000 00000000 00000000 00000001(原码)。

  • b = -1的原因同理。因为char和signed char道理一样。都是有符号字符类型

  • 我们再来看c为什么是255?

11111111 11111111 11111111 11111111 这是-1的补码

重点在于变量c!!

  • c为什么是255?因为c是unsigned char类型,无符号意味着他个正数。最高位不是符号位。无符号数在整型提升的时候和有符号位不一样。它是在前面全补0。
11111111 11111111 11111111 11111111 这是-1补码
11111111 放进变量c截断后的补码
00000000 00000000 00000000 11111111整型提升后的补码
  • 然后以%d的形式打印出来的话,就要把补码转化成原码
00000000 00000000 00000000 11111111整型提升后的补码
但计算机认为最高位是0,所以这时候的补码是个正数。
原码反码补码相同。
  • 所以打印出来就相当于是00000000 00000000 00000000 11111111(即255)。

  • 学到了没有?那我们来检验一下学习成果,再来一道题

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

1.先拿到-128的补码,用我们的原反补计算。

10000000 00000000 00000000 10000000 原码
11111111 11111111 11111111 01111111 反码
11111111 11111111 11111111 10000000 这是-128的补码
  1. 放到char类型中需要截断
10000000 截断后的补码
  1. 发生整型提升后的补码
11111111 11111111 11111111 10000000 补码

4.然后以%u(%u是无符号数)的形式打印出来。
所以计算机认为这个补码就是正数。正数的原码反码补码相同。
所以打印的数就是4,294,967,168
在这里插入图片描述

?浮点型在内存中的存储

  • 首先,我要在这里声明一下,浮点数在内存中的存储规则和整数不一样,他不遵循整数的原码,反码,补码的转换。
  • 只有整数存在原码,反码,补码。

怎么用二进制表示小数

那么计算机怎么存储浮点数(小数)呢?

  • 我们以下面一段代码为例。
#include <stido.h>
int main()
{
	float a = 5.75//二进制数字表示为101.11
	return 0;
}
  • 二进制为101.11怎么转化为十进制的5.75呢?
  • 官方描述: 小数点左边的数字的权是10的正幂,得到整数值,小数点右边的数字的权是10的负幂,得到小数值
    在这里插入图片描述

怎么将二进制小数存入内存

  • 我们要以IEEE标准754在内存中来存入浮点数
  • 简介
    在这里插入图片描述
  • **浮点数的存入标准(IEEE标准754)**是在1976由加州大学伯克利分校的Kahan教授作为顾问设计的。
  • 目前,实际上所有的计算机都支持这个后来被称为IEEE浮点的标准。这大大提高了科学应用程序在不同机器上的可移植性。

IEEE浮点表示方法

  • 接下来以float类型来说明。我们还是拿float a = 5.75为例。探究如何将其二进制101.11存入32(bit)位中。
  • IEEE浮点表示方法用到了科学计数法,所以我们要把101.11转化为1.0111 * 2^2(就是1.0111乘以2的2次方)
  • (-1)^S * M * 2^E
  • (-1)^S表示符号位,当s=0,V为正数;当s=1,V为负数。
  • M(M>=1&& M<2)表示有效数字.(为什么取值范围是1到2?),因为2进制只有0和1来表示,不会出现大于2的情况。
  • E表示指数。
  • IEEE 754规定:
    对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。

    在这里插入图片描述

试图将5.75按照IEEE标准存入变量a(32bit)

#include <stido.h>
int main()
{
	float a = 5.75//将1.0111 * 2 ^2即(5.75的二进制)存储到变量a中
	printf("%f",a);//将a读取并打印。
	return 0;
}
  • 一、转化为IEEE标准为:根据公式(-1)^S * M * 2^E代入
  • 得出 (-1)^0 * 1.0111 * 2 ^2
  • 二、但是根据IEEE 754对有效数字M和指数E的一些特别规定。
  1. 对于有效数字M

在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存小数点后面的部分。比如保存1.0111的时候,只保存0111,等到读取的时候再把前面的1加上去.
(目的是节省1位有效数字,使得5.75这个十进制数更精确)

  1. 对于指数E。现在我们是存储。(比较简单)
  • 读取的时候分三种情况,比较复杂,请耐心看(IEEE就是这样规定的,博主水平有限,没有深层次的了解)。
  • 首先,E是一个无符号整数,E为八个bit位,取值范围0 ~ 255。
  • IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127。
  • 比如,2^2的E是2,所以保存成32位浮点数时,必须保存成2+127=129,即1000 0001。
  • 三、开始存入
  • 首先 ,得到上面5.75的S,M,E各自的值。
  • S存0或1。
  • 将E加上中间数1127之后,转换成二进制的形式存入E的位置。
  • 至于M,将其小数点后面数字存入M位置的最左端,右边剩余的位置补0
    对上面的话翻译。
    1.因为5.75为正数,所以s的位置为0
    2.E为2,然后加上127,为1000 0001。存入E位置上
    3.M为小数点右边的数字即(0111),后面的剩余位置补0
    如下图所示
    在这里插入图片描述

试图将5.75读取并printf

#include <stido.h>
int main()
{
	float a = 5.75//将1.0111 * 2 ^2即(5.75的二进制)存储到变量a中
	printf("%f",a);//将a读取并打印。
	return 0;
}
  • 读取内存的方式关键在于E
  1. E不全为0或不全为1
  • 将指数E的计算值减去127,得到真实值,再将有效数字M前加上第一位的1
  • 变量a中的5.75就是这种情况。
  1. E全为0(即八个bit位都为0000 0000)

指数E等于1-127,得到真实值。有效数字M不再加上第一位的1,而是还原为 0.xxxxxx 的小数。这样做是为了表示±0,以及接近于0的很小的数字

例如.

float b = 9;
printf("%f",b);//b的值为0.000000(为什么会是0?)
  • 我们拿到在内存中b = 9的32个bit位。
 s=0   E=00000000   M=00000000000000000001001

在这里插入图片描述

  • 我们可以发现变量b的E的位置为全0。所以可以写为
  • (-1)^0 × 0.00000000000000000001001×2^(-126)
    就是=1.001×2^(-146)
  • 因为2的-146次方非常小了,接近0,而且M也接近0,且float类型只保留6位有效数字,所以结果为0.000000
  1. E全为1(不再举例子了)

这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);

总结

  • 理解数据在内存中的存储有助于做编程题时出现一个类型0.000000的数时不至于手慌脚乱。
  • 由于编码的长度有限,与传统整数和实数运算相比,计算机运算具有完全不同的属性。
  • 当超出表示范围时,有限长度能够引起数值溢出。当浮点数非常接近于0.0,从而转换0时,也会下溢。

如果你觉得我的文章对你有帮助?欢迎关注?点赞?收藏⭐️留言?。

.
.

参考文献:
《深入理解计算机系统》 2016年机械工业出版社 .(美)布赖恩特(Bryant,R.E.)

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>