十九、数据在内存中的存储

👉 欢迎阅读这篇文章 👇

目录

1、整数在内存中的存储

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

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

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

负整数的三种表⽰⽅法各不相同。

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

对于整形来说:数据存放内存中其实存放的是⼆进制的补码。

补充:对于一个数值,会有前缀和后缀修饰

  • 前缀:表示这个数值的进制类型(0b二进制、0x十六进制、0八进制)
  • 后缀:表示这个数值的类型(signed int、unsigned int...)
    我们平时见到最多的就是既没有前缀又没有后缀的数值,这时就默认代表十进制的有符号整数(int、long、long)。

2、 大小端字节序和字节序判断

我们将整型数据存放在内存后,可以观察到一个细节:

我们把0x11223344存到a里,发现a的地址中是以44、33、22、11的顺序存储的,这就是小端字节序存储。

2.1大小端

超过⼀个字节的数据在内存中存储的时候,就会有存储顺序的问题,按照不同的存储顺序,我们分为**⼤端字节序存储** 和**⼩端字节序存储**。

  • 大端存储模式:是指数据的低位字节内容保存在内存的高地址处。而数据的高位字节内容保存在内存的低地址处。
  • 小端存储模式:是指数据的低位字节内容保存在内存的内存的低地址处。而数据的高位字节内容保存在内存的高地址处。

2.2练习

2.3.1练习1

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

c 复制代码
//设计⼀个⼩程序来判断当前机器的字节序。

int main()
{
	int a = 1;//访问a的地址中第一个字节的值,看是否是1
	if (*(char*) & a)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

2.3.2练习2

c 复制代码
#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;
}
c 复制代码
#include <stdio.h>
int main()
{
	char a = -1;
	//-1是有符号整数,是负整数
	//-1的原码:10000000 00000000 00000000 00000001
	//-1的补码:11111111 11111111 11111111 11111111
	//a是char类型的
	//存到a里的:11111111

	signed char b = -1;
	//-1是有符号整数,是负整数
	//-1的原码:10000000 00000000 00000000 00000001
	//-1的补码:11111111 11111111 11111111 11111111
	//b是 signed char类型的
	//存到b里的:11111111

	unsigned char c = -1;
	//-1是有符号整数,是负整数
	//-1的原码:10000000 00000000 00000000 00000001
	//-1的补码:11111111 11111111 11111111 11111111
	//c是 unsigned char类型的
	//存到c里的:11111111

	printf("a = %d, b = %d, c = %d", a, b, c);
	//a
	//%d:以十进制的形式打印有符号整数。
	//a要传进printf函数里:
	//整型提升:(a是char类型的,假设为有符号类型,补符号位)
	//11111111 11111111 11111111 11111111(以%d有符号规则解读,这个补码对应的数值,就是原码表示的-1)
	//原码:10000000 00000000 00000000 00000001 (十进制:-1)

	//b
	//%d:以十进制的形式打印有符号整数。
	//b要传进printf函数里:
	//整型提升:(b是signed char类型的,有符号类型,补符号位)
	//11111111 11111111 11111111 11111111(以%d的有符号规则解读,这个补码对应的数值,就是原码表示的-1)
	//原码:10000000 00000000 00000000 00000001 (十进制:-1)

	//c
	//%d:以十进制的形式打印有符号整数。
	//c要传进printf函数里:
	//整型提升:(c是unsigned char类型的,无符号类型,补0)
	//00000000 00000000 00000000 11111111(以%d有符号规则解读,这个补码对应的数值,就原码表示的255)
	//原码:00000000 00000000 00000000 11111111 (十进制:255)
	return 0;
}

2.3.3练习3

(1)

c 复制代码
#include <stdio.h>
int main()
{
	char a = -128;
	printf("%u\n", a);
	return 0;
}
c 复制代码
#include <stdio.h>
int main()
{
	char a = -128;
	//-128 有符号整数 负整数
	//原码:10000000 00000000 00000000 10000000
	//补码:11111111 11111111 11111111 10000000
	//a是char类型的
	//存到a里的:10000000
	printf("%u\n", a);
	//a要传进printf里
	//char类型比int小-整型提升
	//a是char类型的-假设是有符号类型的-补符号位(1)
	//11111111 11111111 11111111 10000000(以%u无符号规则解读,都是数值位,根据权重解读为:4,294,967,168)
	return 0;
}

(2)

c 复制代码
#include <stdio.h>
int main()
{
	char a = 128;
	printf("%u\n", a);
	return 0;
}
c 复制代码
#include <stdio.h>
int main()
{
	char a = 128;
	//128 类型:有符号整数 正整数
	//原码:00000000 00000000 00000000 10000000
	//补码:00000000 00000000 00000000 10000000
	//a是char类型的
	////存到a里的:10000000
	printf("%u\n", a);
	//a要传进printf里
	//char类型比int小-整型提升
	//a是char类型的-假设是有符号类型的-补符号位(1)
	//11111111 11111111 11111111 10000000(以%u无符号规则解读,都是数值位,根据权重解读为:4,294,967,168)
	return 0;
}

2.3.4练习4

c 复制代码
#include <stdio.h>
#include <string.h>
int main()
{
	char a[1000];
	int i;
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	printf("%zu", strlen(a));
	return 0;
}

这里是一个char类型的数组,数组里的每个元素都是char类型的,所以我们要先了解char类型的数值的一些内容

  • char/signed char

补码运算:

对char类型的数据运算,(二进制补码:10000000)十进制:-128 减去1就是(二进制补码:01111111)十进制:127(二进制补码00000000)十进制:0减去1就是(二进制补码:11111111)十进制:-1。所以这就是一个循环的过程,从0到-128,然后127-0,然后继续0--128。

上方的代码,就是反映了这个循环过程

strlen函数的功能就是统计字符串中 '\0' 之前的字符的个数。在这里就是统计这个数组元素0之前有多少个元素。经过计算可得有255个。

2.3.5练习5

(1)

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

这里i是一个unsigned char的变量,经过上面的分析可得unsigned char的范围是0~255,而循环条件是i <= 255,会进入无限循环。无限打印hello world

(2)

c 复制代码
#include <stdio.h>
int main()
{
	unsigned int i;
	for (i = 9; i >= 0; i--)
	{
		printf("%u\n", i);
	}
	return 0;
}

这里i是一个unsigned int的变量,unsigned int类型的值一定大于0,而循环条件是i >= 0,会进入无限循环。先从9打印到0,再从一特别大的的值打印到0,一直循环打印。

2.3.6 练习6

c 复制代码
#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;
}

图解:

&a表示整个数组的地址,&a+1指向数组结束后的内存,强制类型转换为int*,赋给ptr1ptr1[-1]*(ptr1-1),即为4。%x,以十六进制形式打印仍是4。

a表示整个数组首元素的地址,a强制类型转换为int,就变成了一个整型值,+1,就是正常的加1,加完1后数值就等于((char*)a)+1,数值就是第一个元素内第二个字节的地址,再强制类型转换为int*后赋给ptr2ptr2指向第一个元素内第二个字节的地址,解引用后,以%x的规则往后读取4个字节的内容,为0x02000000,打印为2000000

3、浮点数的存储

常见的浮点数有:3.14159、1E10等,浮点数家族包括:: floatdoublelong double 类型。

浮点数表⽰的范围: float.h 中定义。

3.1浮点数的存储

根据国际标准IEEE(电⽓和电⼦⼯程协会) 754,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式:

V = ( − 1 ) s × M × 2 E V = (-1)^{s} \times M \times 2^{E} V=(−1)s×M×2E

  • ( − 1 ) s (-1)^{s} (−1)s表示符号位,当S=0时,V为正数;当S=1时,V为负数。
  • M表示有效数字,M是大于等于1小于2的。
  • 2 E 2^{E} 2E表示指数位

举例:

十进制的5.0,写成二进制为101.0,相当于 1.01 ∗ 2 2 1.01*2^2 1.01∗22,这时S=0,M=1.01,E=2。

十进制的-5.0,写成二进制为-101.0,相当于 − 1.01 ∗ 2 2 -1.01*2^2 −1.01∗22,这时S=1,M=1.01,E=2。

IEEE 754规定:

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

3.1.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位有效数字。

⾄于指数E,情况就⽐较复杂
首先,E为一个无符号整数(unsigned int)

这意味着,如果E为8位,那么E的取值范围为0~255;如果E为11位,那么E的取值范围为0~2047。但是我们知道科学计数法时E可能出现会负数,比如:0.5,写成二进制为0.1,转成科学计数法是 1 ∗ 2 − 1 1*2^{-1} 1∗2−1,这时E就为负数。所以IEEE 754规定,存⼊内存时E的真实值必须再加上⼀个中间数 ,对于8位的E ,这个中间数是127 ;对于11位的E ,这个中间数是1023

⽐如, 2 10 2^{10} 210 的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001

这样的浮点数存储⽅式很巧妙,但是我们也要注意到有的浮点数是有可能⽆法精确保存的。⽐如:1.2。

通过调试可以看出,1.2在内存中并没有精准的存储。

3.1 2浮点数取的过程

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

E不全为0或不全为1(常规情况)

这时,浮点数采用将指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上1。

E全为0

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

E全为1

表示一个正负无穷大的数(取决于符号位s)

3.2练习

c 复制代码
#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("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	return 0;
}
c 复制代码
#include <stdio.h>
int main()
{
	int n = 9;
	//9存储在整型变量n中,是一个有符号整数,正整数
	//00000000 00000000 00000000 00001001
	float* pFloat = (float*)&n;
	//将n的地址转为float*类型的存储在pFloat中,pFloat指向的就是一个浮点型数据
	printf("n的值为:%d\n", n);//9
	printf("*pFloat的值为:%f\n", *pFloat);
	//pFloat指向的是一个浮点型数据,就会把00000000 00000000 00000000 00001001当作浮点型来解读
	//s = 0 E位全0 一个确定的、极小的正数 所以⽤⼗进制⼩数表⽰会被四舍五入为就是0.000000。

	*pFloat = 9.0;//将一个浮点型数据9.0赋给*pFloat,pFloat指向n
	//9.0写成二进制就为1001.0即1.001*2^3
	//s = 0 M = 001 E = 3(3+127 = 130)
	//0 10000010 0010000000000000000000
	printf("n的值为:%d\n", n);
	//以%d的规则解读0 10000010 0010000000000000000000,这个补码表示的值,直接按照权重解读得到十进制数1,091,567,616
	//01000001 00010000 00000000 00000000(十进制:1,091,567,616)
	printf("*pFloat的值为:%f\n", *pFloat);//按照%f的规则,就正常打印浮点数9.000000
	return 0;
}
相关推荐
basketball6161 小时前
C 的 malloc/free 与 C++ 的 new/delete 一些区别
c语言·开发语言·c++
iiiiyu1 小时前
⾯向对象和集合编程题
java·大数据·开发语言·数据结构·编程语言
郝学胜-神的一滴1 小时前
Qt 高级开发 006: 架构全解 + 高效学习指南
开发语言·c++·qt·程序人生·架构
爱编码的小八嘎1 小时前
MFC深入-消息映射的实现
c语言
Achou.Wang1 小时前
Concurrency patterns - Go 并发模式
开发语言·后端·golang
存在morning1 小时前
【GO语言开发实践】三 GO 工程化快速上手
开发语言·后端·golang
雁迟1 小时前
第七章:R 向量用法(最核心数据结构)
开发语言·数据结构·r语言
Achou.Wang1 小时前
Go语言并发编程中的死锁防范与破解之道
服务器·开发语言·golang
我命由我123452 小时前
Visual Studio - Visual Studio 注释快捷键
java·c语言·开发语言·c++·ide·java-ee·visual studio