数据在内存中的存储

基本数据类型

整数在内存中的存储

整数在二进制中有两种表示方法,即原码,反码,补码

有符号整数的表示方法同上,只不过最高位表示符号位,0表示正数,1表示负数

正整数的原码,反码,补码的表示方法均相同

原码

原码就是将其他进制的数字直接转换成二进制数字而来的数字,例如数字5直接转换成二进制数字就是101,101就是一个二进制原码

反码

将原码的各个位按位取反就得到了反码,例如101的反码就是010

补码

反码+1就得到补码

计算机存储数据的过程

计算机输入-1  —>  转换成原码1000 0001  —>  转换成反码1111 1110  —>  转换成补码1111 1111  —>  存储进计算机

对于整形数据来说,数据在内存中存放的是补码

我们上面说过,原码、反码、补码的表示对于正数来说都是一样的,而对于负数来说,三种码的表示确是完全不同的,那大家是否会有个疑问:如果原码才是我们人类可以识别并用于直接计算的表示方式,那为什么还会有反码和补码?计算机直接存储原码不就完事了?

在着手解决这些问题之前,让我们先深入理解一下计算机底层的工作原理。虽然对于我们人类而言,识别一个机器数中的首位为符号位显得直观而自然,但在计算机硬件电路设计层面,要实现这一功能却是一项艰巨且复杂的工作。为了简化计算机底层设计,研究者们开始寻求一种新的方式,即将符号位纳入到实际运算过程中。

他们发现,通过采用特定的运算规则,可以使得复杂的减法运算转化为相对简单的加法运算。例如,当我们想要从1中减去1时,实际上可以转换为1加上-1的操作,即1 - 1 = 1 + (-1) = 0。这样一来,不仅大大简化了计算机的运算过程,同时也成功地让符号位得以在运算中发挥作用,从而极大地提升了计算机处理带符号数的效率与便利性。

当我们使用原码进行计算的时候,会发现下面的问题:

1+(-1)=0

0000 0001

+

1000 0001

=

1000 0010==>-2

我们发现如果用原码计算的话其结果是-2,这是何等的荒谬!!!

为了解决这一问题,出现了反码

此时我们再次计算一下

0000 0001(正数的原码,反码,补码都一样,这里不要忘记哦)

+

1111 1110

=

1111 1111(反码)==> 1000 0000==> -0

我们会发现,通过反码来计算结果,是正确的,可是+0和-0都是表示0,难道一个0就要有1000 0000和0000 0000这两种表示方法吗?这未免对其他数字也太不公平了吧!!

因此,出现了补码(救世主来啦)!!!

此时我们再用补码来运算一下:

0000 0001

+

1111 1111

=

0000 0000

现在我们可以观察到,经过计算后得出的结果恰巧是不带符号的0,这无疑验证了补码机制的有效性和正确性。接下来,进一步探讨补码的重要作用,我们可以通过实例揭示补码如何巧妙地表示负数。

对于二进制数系统,我们发现可以利用补码形式来表示-128这个特殊的数值。其推导过程如下所示:

首先,我们有表达式 (-1) + (-127) = -128。

以二进制形式表示这两个数,分别为原码形式下的 [1000 0001](代表-1)和 [1111 1111](代表-127)。

然后,我们将这两个数转换为对应的补码形式,得到 [1111 1111] 和 [1000 0001]。

当这两者相加时,我们得到补码形式的 [1000 0000],这恰好对应于十进制数值-128。

值得注意的是,由于在补码系统中,原本用于表示-0的补码被重用以表示-128,因此-128没有独立的原码和反码表示。只需确认补码为 [1000 0000],即可确定它所代表的十进制数值为-128。

补码的独特优势在于其能额外表示-128这一数值,而在计算机内存中,正是采用补码的形式来存储8位二进制数。因此,一个8位二进制数所能表示的数值范围是通过补码体现的,即[-128,127],而非通过原码或反码所能表示的[-127,127]区间。

这也恰恰解答了我们最初的问题,为何在计算机内部采用原码、反码和补码的不同表示方法。究其原因,是为了使符号位能够融入到运算过程中,同时简化计算机底层的计算逻辑,从而创造出更为高效便捷的数据存储和运算体系。

大小端字节序和字节序的判断

概念

字节序——大于一个字节类型的数据在内存中的存放顺序。是在跨平台和网络编程中,时常要考虑的问题。

分类

字节序通常划分为两种类型:

1. 大端序(Big-Endian):这种排列方式下,一个多字节数据在内存中的存储顺序是从高位字节到低位字节,也就是说,最左边的字节,即最高有效字节,会被存放在内存的较低地址处,而随着地址的增长,字节的权重逐渐降低。

2. 小端序(Little-Endian):与此相反,小端序的存储规则是将低位字节存储在内存的低地址部分,随着地址的递增,字节的权重逐次升高,这意味着最右边的字节,即最低有效字节,会优先存储在内存的低地址位置上。

这里不理解没关系,我们继续往下看,后面就会慢慢理解了。

高低地址与高低字节

高低地址

在讨论C程序的内存空间组织时,我们可以描绘出如下的布局概览:

从最高可用的内存地址 0xFFFFFFFF 开始向下:

首先是 栈区,它遵循自顶向下的增长方式,也就是说,栈的底部位于较高的内存地址,而随着函数调用和局部变量的压入,栈顶会逐渐向较低的内存地址延伸。

紧接着,朝着更低的内存地址方向,我们会遇到 **堆区**,它的特点是自底向上扩张,也就是从相对较低的内存地址开始,随着 `malloc` 或 `calloc` 等动态内存分配函数的调用,堆空间会逐渐向上增长至更高的内存地址。

再往下则是 **全局区**,这部分内存区域包含了全局变量和静态变量,以及常量存储区域,它们在整个程序运行期间始终存在。

最后,在内存地址空间的最低端,坐落着 代码区,这里存储的是可执行程序的机器指令,也是程序执行流程的起点,地址始于 0x00000000。

高低字节

在任何数制系统中,包括十进制以及其他进制如十六进制

通常约定“靠左”代表高位,“靠右”代表低位。

例如,在十六进制数 0x12345678 中,其从高位到低位的字节顺序是这样的:最左边的两位 0x12 是最高有效字节,接下来是 0x34,然后是 0x56,最右边的两位 0x78 则是最低有效字节。所以,按照从高位到低位的顺序解剖该十六进制数,我们得到四个连续的字节分别为 0x12、0x34、0x56 和 0x78。

举例

对于数据 0x12345678,假设从地址0x4000开始存放,在大端和小端模式下,存放的位置分别为:

内存地址    小端模式    大端模式
0x4003    0x12    0x78
0x4002    0x34    0x56
0x4001    0x56    0x34
0x4000    0x78    0x12

判断机器的字节序

我们可以通过一个简单的代码来判断机器是大端还是小段,代码如下

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>

int main()
{
	int i = 1;
	char* a = i;
	if (a == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

这个代码,我们通过强制类型转换,将整型变量1转换为字符串,这样我们就可以通过其打印出来的数值来判断机器是大端还是小段了 

浮点数在内存中的存储

抛砖引玉

我们在这里先抛出一段代码,学习完浮点数在内存中的存储我们就能完美地知道这段代码的运行结果了

在32位的情况下运行下面的代码

#define _CRT_SECURE_NO_WARNINGS 1

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

当我们讨论如何在计算机内存中高效地表示较大的数值时,科学计数法是一个非常实用的方法,它同样适用于二进制环境下浮点数的表达。对于十进制数,我们可以轻松写出 \(2.3 \times 10^5\) 这样的形式;而在二进制系统中,则是用 \(2^n\) 替换 \(10^n\) 来调整小数点的位置。

现在,让我们通过一个具体的示例来阐明这一点。

假设我们要将十进制数 \(0.5\) 转换为二进制浮点数表示。直观上来看,\(0.5\) 等于 \(1\) 除以 \(2\),所以在二进制中它的表示应当含有一个小数点后的 \(1\),其权重为负指数 \(2^{-1}\),因此正确的二进制表示是 \(0.1\)*\(2^{-1}\),即 \(0.1_2\),转换回十进制后确实就是 \(0.5\)。

接下来考虑十进制整数 \(5\) 的情况。首先将其转换为二进制,得到 \(5_{10} = 101_2\)。为了模拟科学计数法,我们将 \(101\) 写成 \(1.01\),并指出小数点之后需要移动的位置。由于 \(5\) 是 \(1\) 后跟两个零,这意味着我们需要将小数点向右移动两位,这相当于 \(1.01_2 \times 2^2\)。在这个二进制表达式中,乘以 \(2^2\) 意味着原数值扩大了四倍,恰巧使 \(1.01\) 变为了 \(101\),对应的是十进制下的原始数值 \(5\)。

简而言之,在二进制浮点数表示中,我们不仅要注意整数部分的转换,还要精细处理小数部分,并利用基数 \(2\) 的幂来精确地定位小数点的位置,以便准确表达任何给定的十进制数。这样的表述方式确保了计算机能够有效地存储和处理各种实数。

国际标准IEEE 754 

任意一个二进制数V可以表示成以下形式;

其中当S为0时表示正数,S为1时表示负数

M是1到2之间的数字

E可正可负

如此,上面5的表示方法可以写成(-1)^0*1.01*2^2

其中,S=0,M=1.01,E=2

这样一来就可以表示浮点数了

浮点数在32位和64位下的存储方式

IEEE754规定

在32位的环境下,最高位S为符号位,接着的8位存储指数E,剩下的23位存储有效数字M

在64位的环境下,最高位S为符号位,接着的11位存储指数E,剩下的54位存储有效数字M 

浮点数的存储过程

IEEE754规定,在存放M的时候,由于M必然是大于等于1,小于2的数字,它的正数位必然会有一个1,那么我们在存储的时候就会把这个1给省略掉,等到拿出来用的时候,再把这个1添上去,这样一来M就可以多存一位。以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不全位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 0 01111110 00000000000000000000000

E全为0

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

1 0 00000000 00100000000000000000000

也就是说,一个数要减去上127或者1023才能变成0,可想而知这个数有多小,小数点要移动多少位,所以这个数就无限趋向于0了

E全为1

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

1 0 11111111 00010000000000000000000

也就是说一个数要减去127或者1023才能变成1,那么这个数字也是趋向于无穷大的

回到题目

#define _CRT_SECURE_NO_WARNINGS 1

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

其运行结果如下,我们逐个分析

首先是n的值不用多说,一定是9

那么为什么9转变成浮点数就变成0.000000了呢?

首先9的二进制序列如下

0000 0000 0000 0000 0000 0000 0000 1001

然后我们将其拆分为浮点数,用上刚刚的知识,最高位0是符号位,表示正数

后面的8位指数E=00000000,有效数字M=000 0000 0000 0000 0000 1001

由于E的指数全为0,所以符合E全为0的情况,所以表示的数就是0.000000

V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146)

那么为什么浮点数9.0按照%d的格式打印会变成这么大的数字呢?

首先,浮点数9.0 等于⼆进制的1001.0,即换算成科学计数法是:1.001×2^3

所以: 9.0  =  (−1)   ∗ 0  (1.001)  ∗  23 ,

那么,第⼀位的符号位S=0,有效数字M等于001后⾯再加20个0,凑满23位,

指数E等于3+127=130, 即10000010

9.0转换成二进制表示应该是0 10000010 001 0000 0000 0000 0000 0000

当这个二进制数被当作整数解析的时候,就是解析其补码形式,解析出来就是这么大的一个数

最后一个对于浮点数的解引用之后依然是浮点数,那么就可以正常地打印出9.0

通过这个题目相信大家一定对数据在内存中的存储有了更深刻的理解了吧

结语

关于内存中数据的存储奥秘,我们尚有许多精彩的知识等待挖掘与分享,期待我们在未来的日子里再次相聚,共同探索这一领域的无尽智慧。在此,暂且道一声:下次见,亲爱的伙伴,挥手说声再见吧!

  • 39
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

阿梦Anmory

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

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

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

打赏作者

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

抵扣说明:

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

余额充值