数据在内存中的存储(C 语言)

一、整数在内存中的存储

1. 原码、反码和补码

在前面的博客中已经提过,整数的二进制表示方法有 3 种:原码、反码和补码。而整数在内存中存储的是它的补码,其中正整数的原码、补码和反码相同。但是,负整数的反码和补码需要通过计算得出。

负整数的补码计算规则:
(1)原码除符号位取反,得到反码
(2)反码 +1 得到补码
而补码按照上述计算再来一遍就能重新得到原码,即补码取反加 1 得到原码;也可以减 1 再取反。

举例说明:
int a = -10;
原码:10000000000000000000000000001010
反码:11111111111111111111111111110101
补码:11111111111111111111111111110110
而在计算机中内存中,一般表示为十六进制,所以该补码应该为:
0x ff ff ff f6(实际上没有空格,这里为了方便观看)

下面我们在 vs2022 中检验上述代码:
在这里插入图片描述
我们发现内存中的十六进制和我们计算出来的十六进制补码是一样的,只不过内存中的十六进制是按照字节倒着排列的。这就涉及内存存储的字节顺序问题了,也就是大小端存储方式。

2. 大端字节序和小端字节序

简单来说,大端字节序就是高位字节存储在低地址,低位字节存储在高地址;而小端字节序就是低位字节存储在低地址,高位字节存储在高地址。如下:
在这里插入图片描述

3. 如何判断大小端字节序

我们来看整数 1 的 8 位十六进制:0x00000001,我们取出其首字节的地址,然后解引用,如果结果为 1,则是小端字节序,如果结果为 0,则为大端字节序。如下代码:
在这里插入图片描述
从前面检验的代码图片中,可以验证上述代码的处理是正确的。

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

运行结果如下:
在这里插入图片描述
首先 -1 的 32 位二进制补码为:
11111111111111111111111111111111
赋值给 a、b 和 c 时均产生截断,只保留最后一个字节。然后输出时,按照 %d 的格式输出,这是又产生整型提升,有符号数补符号位,符号数补 0,则:
a:11111111111111111111111111111111
b:11111111111111111111111111111111
c:00000000000000000000000011111111
所以分别输出 -1,-1,255

(2)

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

运行结果如下:
在这里插入图片描述
-128 的 32 位二进制补码为:
10000000000000000000000010000000
然后赋值给 a 产生截断,保留最后一个字节,最后以 %u 输出时发生整型提升,补符号位 1,得:
11111111111111111111111110000000,结果为 4294967168

(3)

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

这道题和上一道题答案一样,就不过多叙述,大家按照上一题的方法自己试一下。虽然符号位不同,但是最后一个字节都是一样的。

(4)

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

运行结果如下:
在这里插入图片描述
这里需要注意 char 类型的范围,在我的编译器上面 char 是 signed char 类型,其值为 -128 - 127,数组元素的值随着下标的增长从 -1 到 -128,然后减 1 变成 127,再到 0,也就是空字符。前面一共 255 个字符,所以结果为 255。

(5)

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

这里就不显示运行结果了,该代码死循环。首先,unsigned char 类型的取值范围是 0 - 255,不可能出现负数,当 0 再减 1 时就变成了 255,255 加 1 时就变成了 0,所以循环变量 i 最后一次加 1 时,255 + 1 = 0,继续新一轮循环。

(6)

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

运行结果如下(切记在 x86 环境下运行哈,32 位就行,在 64 未环境下会产生截断,无法正常运行):
在这里插入图片描述

那我通过画板来讲解一下这道题:
在这里插入图片描述
然后通过计算,int *ptr1 = (int *)(&a + 1),得出指针 ptr1 指向数组 a 的尾后元素,并把它往后的四个字节解释为一个 int 类型,但是在最后一条语句时,使用 ptr1[-1] 进行输出,也就是 *(ptr1 - 1),此时该结果是数组的最后一个元素 a[4],然后使用 %d 进行输出,结果为 4。如下图:
在这里插入图片描述

而由 int *ptr2 = (int *)((int)a + 1) 得出,指针 ptr2 数组第一个元素的第二个字节,也就是 a[0] 的第二个字节,并把它往后的四个字节解释为一个 int 类型。如下图:
在这里插入图片描述

这样我们得到该小端字节序的 4 个字节:00 00 00 02,转换为正常字节序:02 00 00 00,也就是屏幕中打印的 2000000

这里需要注意,虽然指针 ptr1 已经越界,但是并没有对其进行解引用操作,所以不会造成越界访问。

二、浮点数在内存中的存储

在 C 语言中,浮点数有两种表现形式:
(1)十进制小数形式,如:1.0,3.14等
(2)指数形式(科学计数法形式)2e10,3.14E2,其解释为:2e10 = 2.0*10^10,3.14E2 = 3.14*10^2

浮点数家族包括:float、double 和 long double
浮点数的表示范围和整数一样都定义在一个头文件中,整数的范围在 limits.h 头文件中,浮点数在 float.h 头文件中。

我们先来看一段代码:

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

相信大家看完上述代码以后,对输出结果有了一些猜想,现在来看一下程序的执行结果:
在这里插入图片描述
相信大家对于这个输出结果一定有许多疑惑,这个和浮点数在内存中的存储有关,我们先来看一下浮点数在内存中是如何存储的,然后再回过头来简述上述代码。

1. 浮点数在内存中存储的规定

根据国际标准IEEE(电⽓和电⼦⼯程协会)754,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式:
V = (−1) ∗ S M ∗ 2E
(1)(−1) 表⽰符号位,当S=0,V为正数;当S=1,V为负数S
(2) M表⽰有效数字,M是⼤于等于1,⼩于2的
(3)2E 表⽰指数位

举例来说:
⼗进制的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

在这里插入图片描述
在这里插入图片描述

1.1 浮点数存的过程

IEEE754对有效数字M和指数E,还有⼀些特别规定。
前⾯说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中 xxxxxx 表⽰⼩数部分。
IEEE754规定,在计算机内部保存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是可以出现负数的,所以IEEE754规定,存⼊内存时E的真实值必须再加上⼀个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。⽐如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

1.2 浮点数取的过程

指数E从内存中取出还可以再分成三种情况:

(1)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

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

(3)E全为1
这时,如果有效数字M全为0,表⽰±⽆穷⼤(正负取决于符号位s);
1 0 11111111 00010000000000000000000

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

我们先看整数 9 的 32 位 二进制编码:
00000000000000000000000000001001
首先,第一条输出语句使用 %d 按照整数输出没有问题,显示数字 9。而第二条输出语句把该 32 为二进制解释为 float 类型,
0 00000000 00000000000000000001001
可以看到指数部分全部为 0,则 E = 1 - 127 = -126,然后小数部分不加 1,计算 (-1)^0 * 0.00000000000000000001001 * 2^-123 = 0

然后 *pFloat = 9.0 语句是按照 float 类型浮点数的存储方式,把 9.0 存储到了 n 的 32 位二进制中,过程如下:
9.0 的二进制 1001.0 = 1.001 * 2^3,得:
S = 0,E = 3,M = 001,表示为二进制得:
0 10000010 00100000000000000000000
而第三条输出语句使用 %d 打印该数,那就按照整数的方式取出,把该 32 位二进制数按相应的权重计算,然后输出,结果为 1091567616。而第四条语句按照浮点数输出,结果为 9.0。

猜你喜欢

转载自blog.csdn.net/weixin_70742989/article/details/143106483