数据在内存中的存储(了解大小端字节序&浮点数在内存中存储)详细~

目录

1、整数在内存中的存储

2、了解大小端字节序

2.0 为什么有大小端之分呢?

3、练习题

3.1 练习01

3.2 练习02

3.3 练习03

3.4 练习04

3.5 练习05

3.6 练习06

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

4.0 浮点数在计算机内部的表示方法

4.1 浮点数存的过程

4.2 浮点数取的过程

4.3 回到开始时示例的代码,分析:


1、整数在内存中的存储

整数的2进制表示方法有三种,即原码、反码和补码

有符号的整数,三种表示方法均有符号位数值位两部分,符号位都是用0表示“正”,用1表示"负",最高位的一位是被当做符号位,剩余的都是数值位。

正整数的原、反、补码都相同。

负整数的三种表示方法各不相同。

原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码。

反码:将原码的符号位不变,其他位依次按位取反得到反码。

补码:反码+1就得到补码。

对于整型来说:数据存放内存中其实存放的是补码。

原因:使用补码,可以将符号位和数值域统一处理;

同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

2、了解大小端字节序

在计算机科学中,字节序或端序是指多字节数据在内存中的存储顺序。主要有两种字节序:大端字节序(Big-Endian)小端字节序(Little-Endian)

大端(存储)模式: 是指数据的低位字节(LSB)内容保存在内存的高地址处,而数据的高位字节(MSB)内容,保存在内存的低地址处。

小端(存储)模式: 是指数据的低位字节(LSB)内容保存在内存的低地址处,而数据的高位字节(MSB)内容,保存在内存的高地址处。

✅示例: 

#include <stdio.h>
int main()
{
	int a = 0x11223344;

	return 0;
}

调试的时候,我们可以看到在a中的 0x11223344 这个数字是按照字节为单位,倒着存储的。

2.0 为什么有大小端之分呢?

产生原因:

在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节(8位)。然而,在C语言等编程语言中,除了8位的char类型外,还有16位的short型、32位的long型等数据类型。对于位数大于8位的处理器(如16位、32位或64位处理器),由于寄存器的宽度大于一个字节,因此必须解决如何将多个字节安排到内存中的问题。这就导致了大端模式和小端模式的产生。

应用场景:

我们常用的 X86 结构是小端模式,而KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

1、硬件架构:不同的硬件架构可能采用不同的字节序方式。例如,Intel x86系列处理器采用的是小端模式,而MIPS、PowerPC等处理器则采用的是大端模式
2、文件格式:在文件格式中,常常需要使用特定的字节序来表示数据。例如,BMP图像文件中,像素数据通常采用小端模式存储;而WAV音频文件中,样本数据则采用大端模式存储。
3、网络传输:在网络传输数据时,通常需要将数据转换成网络字节序(即大端模式),以确保在不同机器之间的传输中不会出现问题。因此,大多数协议规定了网络字节序应该采用大端模式
4、数据库存储:在数据库中,常常需要对多字节数据类型进行排序和比较。由于不同的字节序方式会影响排序结果,因此在数据库设计中需要考虑字节序问题。

3、练习题

3.1 练习01

设计一个小程序来判断当前机器的字节序。

#include<stdio.h>

// 设计一个小程序来判断当前机器的字节序

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

在VS2022 X86下运行结果:

函数写法:

#include<stdio.h>

// 函数写法
//int check_sys() // 第一种
//{
//	int num = 1;
//	if (*(char*)&num == 1)
//		return 1; // 小端
//	else
//		return 0; // 大端
//}
// 
int check_sys() // 第二种
{
	int num = 1;
	return *(char*)&num; // 返回1是小端返回0是大端
}

int main()
{
	if (check_sys() == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	
}

3.2 练习02

代码:

#include <stdio.h>
int main()
{
	char a = -1;
	signed char b = -1;
	unsigned char c = -1;
	printf("a=%d,b=%d,c=%d\n", a, b, c); // a=-1,b=-1,c=255 
	return 0;
}

解析:

%d —— 打印有符号的整数,%u 打印的是无符号的整数。其中a是有符号的a, b也是有符号的,打印结果一样,而unsigned char 则是无符号的,所以我们先找出-1的补码,而c存储在char时存8位(即11111111),但要以%d的形式打印,需要整型提升,c是unsigned char,无符号位的,高位数补0,结果为(00000000 00000000 00000000 11111111),再以%d打印,打印的是有符号的数 ,符号位又为0,为正数,原反补相同。所以内存存的为这个(00000000 00000000 00000000 11111111)补码的话,原码也一样为(00000000 00000000 00000000 11111111),所以结果为正的255。

运行结果:

char 是有符号还是无符号的这个不太确定,取决于编译器~但是大部分编译器上char==signed char

3.3 练习03

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

解析:

%u —— 打印的是无符号的整数。在%u 的角度,它认为在内存中存储的是无符号的整数。

-128 原码为(10000000000000000000000010000000),求出补码(11111111111111111111111110000000 ),char a 只能存储8位(即10000000)要打印%u的无符号整数,所以要整型提升,char为有符号char ,高位补1,补符号位。结果为(11111111111111111111111110000000)提升完,当作是内存内的补码,%u 打印的是无符号的整数,故原反补相同,所以原码也是(11111111111111111111111110000000),结果用计算机算出为:(4294967168)

改编一下代码:

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

结果一样的:思路跟上面差不多,就不多解析了。

实际上char 类型的取值范围(signed char)为-128~127 ,当你给128 时根本存不下,相当于内存放的就是-128。所以结果一样。如果是无符号char( unsigned char )取值范围是0~255。第一位就不是符号位了。

扩展:( signed short)类似的。

格式决定了我如何看待数据,有时候跟它的类型没太大关系。

如下面的代码,结果是一样的,不建议这样写代码,最好是signed int 就用%d 打印,是unsigned int 就用%u 打印

3.4 练习04

代码:

#include <stdio.h>
#include<string.h>
int main()
{
	char a[1000];
	int i;
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	printf("%zd", strlen(a));// 255
	return 0;
}

strlen 求得是字符串的长度,统计的是 \0 之前出现的字符个数,我们要找到数组中 0 的位置,如图所示,已知char 的范围为-128~127。

3.5 练习05

代码:

循环条件恒成立(恒为真),代码死循环。

#include <stdio.h>
unsigned char i = 0; // 全局变量
// 0~255
int main()
{
	for (i = 0; i <= 255; i++)
	{
		printf("hello world\n"); // 死循环打印hello world
	}
	return 0;
}

unsigned char ,无符号整型最小值为0,循环条件不可能小于0,恒成立,死循环。 

#include <stdio.h>
#include<windows.h>
int main()
{
	unsigned int i;
	for (i = 9; i >= 0; i--)
	{
		printf("%u\n", i);// 死循环
		Sleep(100);// 休眠查看
	}
	return 0;
}

3.6 练习06

代码:

#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);// 4,  2000000
    // // %x 是十六进制打印
	return 0;
}

解析:

&a 则是取出了整个数组的地址,&a+1跳过了整个数组,赋给ptr1 强制类型转换,ptr1 指向同个位置,ptr1[-1]---> *(ptr1-1),整型指针解应用得到 4

a 为数组名,就是首元素的地址,强转为int ,把它当作整数,(int)a + 1为整数+1,其实只是向后偏移一个字节而已。*(ptr2),整型指针解引用,访问四个字节 00 00 00 02,在内存中它是小端存放,要还原成真实值为 0x02 00 00 00,前面的0x0不打印,所以结果为2000000

 

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

常见的浮点数:3.14159、1E10等,浮点数家族包括: float、double、long double 类型。 浮点数表示的范围: float.h 中定义

✅示例:

#include <stdio.h>
int main()
{
	int n = 6;
	float* pFloat = (float*)&n;
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	*pFloat = 6.0;
	printf("num的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	return 0;
}

运行结果:

观察得到:如果我以整数的形式放进去,以浮点数取出,结果不对,反之,我以浮点数的形式放进去,以整数取出结果也不对。但是以整数(浮点数)的形式放进去,以整数(浮点数)取出,结果对。下面的例子说明了整数和浮点数在内存中存储方式有区别。

4.0 浮点数在计算机内部的表示方法

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

V   =  (−1)^s ∗ M ∗ 2^E

(−1)^s 表示符号位,当S=0,V为正数;当S=1,V为负数

M 表示有效数字,M是大于等于1,小于2的

2^E 表示指数位

10进制转为2进制简单示例:

二进制中,其实就是把底数的10变成了2,如在10进制中,1.011x10^2=101.1,而在2进制中,把底数10变成了2,1.011x2^2=101.1

IEEE 754规定:

对于32位的浮点数,最高的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M

对于64位的浮点数,最高的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M

4.1 浮点数存的过程

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位(即1加上尾数位的23位)有效数字。

至于指数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。

     保存为64位浮点数的时候,E保存为10+1023=1033,即10000001001。

4.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,则其二进制表示形式为:

0 01111110 00000000000000000000000

这里S为0,E为01111110,M为00000000000000000000000

又比如:

 下面这两种情况比较少见:

② E全为0

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

③ E全为1

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

4.3 回到开始时示例的代码,分析:

为什么 6 还原成浮点数,就成了 0.000000 ? 6以整型的形式存储在内存中,得到如下二进制序列:

0 00000000 00000000000000000000110

首先,将 6 的二进制序列按照浮点数的形式拆分,得到第一位符号位S = 0,后面8位的指数 E=00000000 , 最后23位的有效数字M = 00000000000000000000110。由于指数E全为0,符合E为全0的情况。因此,浮点数V就写成:

V=0.00000000000000000000110 * 2^(-126)= 1.10*2^(-147)

显然,V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000。

浮点数6.0,为什么整数打印是1086324736呢?

浮点数6.0的二进制0110.1 换算成科学技术法:1.100*2^2

故:6.0=(-1)^0 * 1.100 * 2^2

所以第一位的符号位S=0,有效数字M等于100后⾯再加20个0,凑满23位,指数E等于2+127=129, 即010000001,写成二进制形式,S+E+M:

 0 10000001 100 0000 0000 0000 0000 0000

这个32位的二进制数,被当做整数来解析的时候,就是整数在内存中的补码,原码也一样,所以最终结果为 1086324736 。

喜欢的话

⛳ 点赞☀收藏 ⭐ 关注!

如有不足欢迎评论区指出~

Respect!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值