数据在内存中的存储

目录

一. 整数在内在存中的存储

二.⼤⼩端字节序和字节序判断

1.什么是大小端?

2.为什么有大小端?

3.练习

(1)判断大小端

(2)练习 - 考察无符号和符号整形提升

(3)练习 - 考察取值范围

(4)练习 - 考察取值范围

(6)练习 - 考察小端存储和指针

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

1.练习

2.浮点数的存储

(1)浮点数存的过程

(2)浮点数取的过程 

3.题目解析

4.有些数字存储的时候打印出数字并不一样


一. 整数在内在存中的存储

数据在内存中是以二进制的形式存储的。

在讲解操作符的时候,我们就讲过了下⾯的内容:
整数的2进制表⽰⽅法有三种,即原码,反码和补码。
有符号的整数:三种表⽰⽅法均有符号位和数值位两部分,符号位都是⽤0表⽰“正”,⽤1表⽰“负”,最⾼位的⼀位是被当做符号位,剩余的都是数值位。

整数在内存存储形式:
正整数的原、反、补码都相同。
负整数的三种表⽰⽅法各不相同。
原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码 + 1就得到补码。
对于整形来说:数据存放内存中其实存放的是补码。
在计算机系统中,数值⼀律⽤补码来表⽰和存储。
原因在于,使⽤补码,可以将符号位和数值域统⼀处理;
同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

二.⼤⼩端字节序和字节序判断

代码举例:

int main()
{
	int a = 10;//0x 0a 00 00 00
	int b = 0x11223344;//1个十六进制位是4个二进制位,2位十六进制位占1个字节。
	//调式:44 33 22 11倒着

	return 0;
}

调试的时候,我们可以看到在a中的 0x11223344 这个数字是按照字节为单位,倒着存储的。这是为什么呢?
分析:int b = 0x11223344;
数据存放有不同方式存储 - 最大意义是方便我们使用。

正:11 22 33 44 大端/*字节序*/存储 - 以字节为单位讨论它们顺序的,不是以bit位。 - 正着放正着还原。
倒:44 33 22 11 小端/*字节序*/                                                                                    - 倒着放倒着还原。
11 33 22 44 / 11 44 22 33 / 44 11 22 33...这些顺序很乱不方便我们记,所以我们不会使用。

总结:

1.一个数据在内存存储的时候所需的空间超过一个字节,就要讨论顺序的问题。一个char类型的值就不需要讨论顺序。
2.小端字节序存储和大端字节序存储,只是存储的顺序不一样,常见的存储的顺序就这2种。
3.没啥nb的地方,就是数据在内存存储的不同形式而已。

1.什么是大小端?

其实超过⼀个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为⼤端字节序存储和⼩端字节序存储。
下⾯是具体的概念:
⼤端(存储)模式:是指数据的/*低位字节*/内容保存在内存的/*⾼地址处*/,⽽数据的⾼位字节内容,保存在内存的低地址处。
⼩端(存储)模式:是指数据的/*低位字节*/内容保存在内存的/*低地址处*/,⽽数据的⾼位字节内容,保存在内存的⾼地址处。
上述概念需要记住,⽅便分辨⼤⼩端。

2.为什么有大小端?

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着⼀个字节,⼀个字节为8bit 位,但是在C语⾔中除了8 bit 的 char 之外,还有16 bit 的 short 型,32 bit 的 long 型(要看
具体的编译器),/*另外,对于位数⼤于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度⼤于⼀个字节,那么必然存在着⼀个如何将多个字节安排的问题。因此就导致了⼤端存储模式和⼩端存储模式。*/
例如:⼀个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么
0x11 为⾼字节, 0x22 为低字节。对于⼤端模式,就将 0x11 放在低地址中,即 0x0010 中,
0x22 放在⾼地址中,即 0x0011 中。⼩端模式,刚好相反。我们常⽤的 X86 结构是⼩端模式,⽽
KEIL C51 则为⼤端模式。很多的ARM,DSP都为⼩端模式。有些ARM处理器还可以由硬件来选择是⼤端模式还是⼩端模式。

3.练习

(1)判断大小端

请简述⼤端字节序和⼩端字节序的概念,设计⼀个⼩程序来判断当前机器的字节序。(10分) - 百度笔试题
代码举例:
写法:1

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

	}
	return 0;
}

写法:2

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

	}
	return 0;
}

写法:3

int check_sys()
{
	int a = 1;
	//1.取出a的地址
	//2.强制类型转换成char*后解引用,只取a的第一个字节的数据
	//3.如果取出是1,就是小端,取出是0就是大端。
	return *(char*)&a;
}
int main()
{
	int ret = check_sys();
	if(ret == 1)
		printf("小端\n");
	else
		printf("大端\n");

return0;

}

分析:
1.
00 00 00 01;如果是大端第一个字节是0 
01 00 00 00;如果是小端第一个字节是1 - 只要拿到第一个字节判断就行。
2.怎么拿到第一个字节?用char*的指针解引用,但是强制转换不行。
3.直接强制类型转换不行,不管以什么顺序存放,大端还是小端都是取的是低位的数据。

(2)练习 - 考察无符号和符号整形提升

代码举例:

int main()
{
	char a = -1;//-1是整形存放在有符号char里面 - 1.写出补码
	//10000000000000000000000000000001 - 源码
	//11111111111111111111111111111110 - 反码
	//11111111111111111111111111111111 - 补码;这是-1的补码要放在char类型a里面去 - 2.要截断
	// 存储在a中要发生截断
	//11111111 - a
	//3.有符号整形提升 - 提升按照符号位提升,补满一个整形
	//11111111111111111111111111111111 - 提升
	//10000000000000000000000000000001- 4.提升完,%d打印补码转换成源码
	//
	signed char b = -1;//signed char也是符号char,1.写出补码 2.要截断
	//11111111 - b
	//
	unsigned char c = -1;
	//11111111 - c
	//3.无符号整形提升 - 高位补0,补满一个整形
	//00000000000000000000000011111111 - 提升
	//4.整数的源码,反码,补码相同
	//
	printf("a=%d,b=%d,c=%d", a, b, c);//a=-1;b=-1;c=255;
	//%d - 十进制的形式打印有符号的整数,最高一位会当成符号位打印。

	return 0;
}

总结:
1.要对signed和unsigned要了解。
2.大部分编译器都是有符号char,signed char在vs上是有符号char。

int main()
{
	char a = -128;
	//1.先考考虑这个变量能不能存的下-128 - 放的下
	//10000000000000000000000010000000
	//11111111111111111111111101111111
	//11111111111111111111111110000000
	//2.截断
	//10000000 - a
	//3.%u打印要整形提升 - 提升自己的类型
	//11111111111111111111111110000000 - %u打印 - 4294967168
	//4.提升是有符号的数,因为char自己类型是有符号char,%u打印无符号数 - 无符号数没有源反补的概念

	printf("%u\n", a);//4294967168
	printf("%d\n", a);//-128
	//%u - 十进制的形式打印无符号的整数。

	return 0;
}

总结:有符号数字最好用有符号的打印,无符号数字最好用无符号的打印,不然很有可能不是自己期望的结果。
代码举例:2

int main()
{
	char a = 128;
	//1.先考考虑这个变量能不能存的下128 -放不下
	//00000000000000000000000010000000
	//2.截断
	//10000000 - a
	//3.整形提升 - 
	//11111111111111111111111110000000 - %u打印 - 4294967168
	//
	printf("%u\n", a);//4294967168

	return 0;
}

总结:char里面存的下-128,当你存128是存不下的,a会当成-128,是个轮回。

知识点补充:signed char 和 unsigned char 取值范围探究
signed char a;//取值范围 - 在内存中存储的所有8个二进制的可能性 - 高位一定是符号位 - 内存(补码)
00000000//0
00000001//1
00000010//2
00000011
00000100
...
01111111//127
10000000//11111111 -> 100000000 - -128看见这种不需要计算直接被解析成128,为什么,因为是规定。你把128转换成补码也刚好是。
10000001//11111110 -> 11111111 - -127
10000010//11111101 -> 11111110 - -126
...
11111110//10000001 -> 10000010 - -2
11111111//10000000 -> 10000001 - -1
所以siged char最大取值范围:-128 ~ 127。

unsigned char b;//取值范围 - 在内存中存储的所有8个二进制的可能性 - 全是有效位
00000000//0
00000001//1
00000010//2
00000011
00000100
...
01111111//127
10000000//128 - 因为没有负数的概念
10000001//129
10000010//129
...
11111110//254
11111111//255
所以unsigned char最大取值范围0 ~ 255。

总结:是个轮回的效果。 因为127+1 = -128; 255+1 = 0 ,加的是二进制位的补码。

推而广之:
short:-32768 ~ 32767 / 0 ~ 65535
int:
long:
~取值范围

(3)练习 - 考察取值范围

代码举例:

int main()
{
	char a[1000];
	//1.先考虑char a 的取值范围-128 ~ 127
	//2.1000个元素找到'\0'就停
	//从负数开始 -1 -2 -3 ... -128 127 126 125 ... 5 4 3 2 1 0 -1 -2 ... -128直到放完1000个元素为止。
	//128 + 127 = 255
	//
	int i;
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	printf("%d", strlen(a));//255

	return 0;
}

注意:char 里面的1000个元素是放不到-1 ~ -1000的值的。他会在char里面的取值范围循环找到。

(4)练习 - 考察取值范围

代码举例:

unsigned char i = 0;
int main()
{
	for (i = 0; i <= 255; i++)
	{
		printf("hello world\n");//死循环
	}
	return 0;
}

分析:
1.先考虑unsigned char 的取值范围 0 ~ 255,存的最大值是255。
2.255再+1变成0;1111 1111 + 1 = 0,又开始从0开始循环到255+1 = 0。
3.结果是一直循环打印hello world。
4.而且聪明的编译器已经告诉你了,定义错误的for-loop。循环无限执行

代码举例:2 - 考察取值范围

int main()
{
	unsigned int i;
	for (i = 9; i >= 0; i--)
	{
		printf("%u\n", i);//死循环
		Sleep(1000);//睡眠函数 - 每打印睡眠1秒
	}

	return 0;
}

分析:
1.unsigned int 是符号整形,没有源反补的概念,全是有效位。
2.当循环到i=0-1的时候,i会等于整形的最大值。4294967295 - 4294967294 - 4294967293。
3.因为无符号数永远得不到最小值会死循环。

(6)练习 - 考察小端存储和指针

int main()
{
	int a[4] = { 1, 2, 3, 4 };
	int* ptr1 = (int*)(&a + 1);
	//取出整个地址+1;指向数组后面的地址
	//
	int* ptr2 = (int*)((int)a + 1);
	//(int)a+1:a是地址被转换成整形加+1;只是让地址+1,注意:数组名是首元素的地址+1跳过一个元素地址。
	//假设:a的地址0x010 = 16;0x10+1=0x11=17
	//0x11又被转换成int*类型;访问一个整形。
	//
	printf("%x,%x", ptr1[-1], *ptr2);//ptr1 = 4 ,ptr2 = 02 00 00 00;
	//ptr[-1] -> *(ptr1 - 1);
	
	return 0;
}

分析:
1.假设当前小端环境:低位放在低地址,高位放在高地址。
2.01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00
3.当int * ptr2 = (int*)((int)a + 1); 解引用访问的是 00 00 00 02。
4.因为是小端存储,打印的时候,怎么放的怎么还原,02 00 00 00。
5.注意((int)a + 1):只是a的地址转换成整形+1,每个字节都有都有地址,只是偏移了一个字节。

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

常⻅的浮点数:3.14159、1E10 = 1.0*10^10等,浮点数家族包括: float、double、long double 类型。
浮点数表⽰的范围: float.h 中定义。

1.练习

代码举例:搞懂这个代码就相当于理解了浮点数在内存中的存储

int main()
{
	int n = 9;
	float* pFloat = (float*)&n;

	printf("n的值为:%d\n", n);//9
	printf("*pFloat的值为:%f\n", *pFloat);//0.000000
//1.&n被强制类型转换成float*类型,同类型相互访问刚好访问完 - 应该输出9.000000才对?为啥0.000000?

	*pFloat = 9.0;
	printf("num的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);//9.000000

	return 0;
}

输出什么?
问题:
1.我以整形的形式存进去一个9,同时整形 % d的形式拿的时候也是9。
如果站在float * 指针%f拿的居然不是9,为啥?
2.以float * 指针解引用形式存进去9.0,以浮点数形式 % f拿到的是9.000000
但是以整形 % d形式拿的时候发现结果不是9.0 为啥?
说明整数的存储的方式和浮点数存储方式是不一样的。

2.浮点数的存储

上⾯的代码中, num 和 * pFloat 在内存中明明是同⼀个数,为什么浮点数和整数的解读结果会差别这么⼤?
要理解这个结果,⼀定要搞懂浮点数在计算机内部的表⽰⽅法。
根据国际标准IEEE(电⽓和电⼦⼯程协会)754 - 就是浮点数内存存储方式,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式:
V = (−1) ^ s * M ∗ 2 ^ E - 浮点数可以化成二进制形式
•(−1)S 表⽰符号位,当S = 0,V为正数;当S = 1,V为负数。
• M 表⽰有效数字,M是⼤于等于1,⼩于2的。
• 2 ^ E 表⽰指数位。

举例:一个浮点数用二进制表示
十进制:5.5 - 用二进制表示
101.101 - 错误形式:因为小数点表示2 ^ -1 = 0.5; 2^-2 = 0.25; 2^-3 = 0.125;加起来大于0.5,所以当要表示0.5用1。
101.1 - 正确形式,但形式不好看,所以要用科学计数法。
1.011 * 2 ^ 2 - 科学计数法; 方法小数点移动几位,就是2的几次方。
(—1) ^ 0 * 1.011 * 2 ^ 2; - IEEE754 浮点数表示形式。
S = 0; M = 1.011; E = 2; 

注意:任何一个浮点数都可以写成IEEE754形式表示,但是不要写的太复杂不然压根写不出来。
比如3.14 - 写成IEEE754形式
0.11.001010100001010101....不管你怎么凑都要差一点点,有时候可能无法精确凑出这个数字的,会一直往下面写,你给的数字太复杂凑起来比较难,011.小数点后面14根本写出2进制形式。

总结:IEEE754表达式:(-1)是固定的,2是固定的,只要S,M,E是变化的。/*所以存储的时候只需要存储S,M,E就可以了*/。

IEEE 754规定:
float 对于32位的浮点数,最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M。
double 对于64位的浮点数,最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M。

(1)浮点数存的过程

1.存M的过程:
IEEE754 对有效数字M和指数E,还有⼀些特别规定。 
M一定是成 1.xxxxxx的形式,M是⼤于等于1,小于2的。所以1没必要存进去。
IEEE 754 规定,在计算机内部保存M时,默认这个数的第⼀位总是1,因此可以被舍去,只保存后⾯的xxxxxx部分。
比如:
保存1.01的时候,只保存01,等到读取的时候,再把第⼀位的1加上去。/*这样做的目的,是节省1位有效数字。*/
//以32位浮点数为例,留给M只有23位,将第⼀位的1舍去以后,等于可以保存24位有效数字,这样会精度更高1位。

2.存E的过程:⾄于指数E,情况就比较复杂
首先,E为⼀个⽆符号整数(unsigned int)
这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。
但是科学计数法中的E是可以出现负数的:
举例:
10进制:0.5
二进制 :0.1
科学计数法:(-1) ^ 0 * 1.0 * 2 ^ -1; E = -1;
真实的E是可能出现负数的,所以IEEE 754规定,存⼊内存时E的真实值必须再加上⼀个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。
比如,2 ^ 10的E是10,所以保存成32位浮点数时,必须保存成10 + 127 = 137,即10001001。

代码举例::
10进制:5.5
二进制:101.1
科学计数法:(-1) ^ 0 * 1.011 * 2 ^ 2;
S = 0;符号位
E = 2 + 127 = 129;
M = 1.011;后面不够23位低位补0。

int main()
{
	float f = 5.5f;
	//0 10000001 01100000000000000000000 - 补码
	//十六进制:40 B0 00 00
	//小端存储:00 00 B0 40
	//
	return 0;
}

(2)浮点数取的过程 

跟存的方式恰好相反,但有一些特殊。
指数E从内存中取出还可以再分成三种情况:

1.E不全为0或不全为1 - 常规还原
这时,浮点数就采⽤下⾯的规则表⽰,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第⼀位的1。

int main()
{
	float f = 5.5f;
	//存
	//0 10000001 01100000000000000000000 - 补码
	//十六进制:40 B0 00 00
	//小端存储:00 00 B0 40 - 存储
	//还原:
	//E:129 - 127 = 2;M:01100000000000000000000 + 1 = 1.01100000000000000000000 
	//1.01100000000000000000000 * 2 ^ 2; - 还原
	//
	return 0;
}

2.E全为0 - 存在内存的值是全0
存储的时候真实的e是加了127里面值为全0,所以e真实值为-127,这个表示几乎正负接近于0的数字。
这时,浮点数的指数E等于1 - 127(或者1 - 1023)即为真实值,有效数字M不再加上第⼀位的1,而是还原为0.xxxxxx的小数。
这样做是为了表⽰±0,以及接近于0的很小的数字。

3.E全为0 - 存在内存的值是全1
存储的时候真实的e是加了127里面值为全255,所以e真实值为128, 这个表示正负无穷大的数字。
这时,如果有效数字M全为0,表⽰±无穷⼤(正负取决于符号位s);

3.题目解析

int main()
{
	int n = 9;
	float* pFloat = (float*)&n;
	//00000000000000000000000000001001 - 9整形存储的补码
	//0 00000000 00000000000000000001001 - 9浮点数存储的补码
	//E为全0 
	//E = 1 - 127 = -126。
	//M = 0.00000000000000000001001 - 不再加+1
	//还原:0.00000000000000000001001*2^- 126 - 是一个无限接近于0的数字。
	//
	printf("n的值为:%d\n", n);//9 - 因为整形存进去,又以整形的形式拿出来,所以是9。
	printf("*pFloat的值为:%f\n", *pFloat);//0.000000 - 因为是个无限接近于0的数字,%f只打印后面6位

	//(-1)^0 * 1.001 * 2 ^ 3;
	//S = 0;M = 1.001;E = 2 ^ 3
	//0 10000010 00100000000000000000000 - 补码
	//S   E+127         M
	//
	*pFloat = 9.0; - //以浮点数形式存储
	printf("num的值为:%d\n", n);//0 10000010 00100000000000000000000 - %d打印
	printf("*pFloat的值为:%f\n", *pFloat);//9.000000 - 因为以浮点数放进去再以浮点数拿的时候就是9.000000

	return 0;
}

总结:
1.有些浮点数在内存中无法精确保存,因为float最多存储M23位有效数字,double最多存储M52位有效数字。
2.double 类型的精度必 float 更高。
3.两个浮点数比较大小的时候直接用 == 比较可能存在问题!因为不能精确的保存,只能自己给个精度了。
比如:

4.有些数字存储的时候打印出数字并不一样

代码举例:

int mian()
{
	float f = 99.7f;
	printf("%f", f);//99.699997
	return 0;
}

总结:
1.不需要背,要理解怎么存储的。
2.整数不要以浮点数形式拿出来,浮点数不要以整数形式拿出来。

  • 18
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值