深度刨析数据在内存中的存储

本文详细解析了C语言中的类型转换,重点介绍了隐式类型转换中的整型提升机制,展示了不同类型在内存中的存储方式,包括大小端字节序判断和浮点数的IEEE 754标准表示。通过实例演示和测试题,帮助理解不同类型和精度的运算规则。
摘要由CSDN通过智能技术生成


前言


一、类型转换

隐式类型转换(整形提升)

我们先来看一个例子

#include<stdio.h>
int main()
{
	char a = 3;
	char b = 127;
	char c = a + b;
	printf("%d\n", c);

	return 0;
}

在这里插入图片描述
为什么它们相加得到得是一个负整数呢?
首先我们知道char类型得取值范围-128~127,3+127超出了它的范围。
它们的补码(关于原码、反码、补码

3:      00000011
127:     111111111
相加:100000010

舍去第一位,只要左边八位,得到的应该是2,但答案却不是如此,说明计算机中不是直接使用字符类型进行运算。

整形提升概念:
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升
整型提升的意义:

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。
所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

如何进行整体提升呢?

整形提升是按照变量的数据类型的符号位来提升的。

负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111

正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001

无符号整形提升,高位补0

总结下来:有符号的数据类型,提升时高位补符号位;无符号的数据类型,提升时高位补0。

原来如此,长度小于int类型(4)的需要提升,那我们之前的问题就可以的到解决了。


int main()
{
	//char类型大小是1字节
	char a = 3;
	//整形3:00000000000000000000000000000011
	//赋值给a,发生截断:00000011 - a
	char b = 127;
	//整形127:00000000000000000000000001111111
	//赋值给b,发生截断:01111111 - b
	
	//
	//a和b都是char类型,自身大小的都是1byte,所以这里计算的时候要进行整型提升
	//
	//00000000000000000000000000000011 - a的整型提升
	//00000000000000000000000001111111 - b的整型提升
	//00000000000000000000000010000010 - 相加的结果
	//
	char c = a + b;
	//赋值给c,发生截断:10000010 - c
 	//整型提升后因为char是有符号数据,符号位是1,需要转为原码
	//11111111111111111111111110000010 - c的整型提升 - 补码
	//11111111111111111111111110000001 - 补码-1 -反码
	//10000000000000000000000001111110 - 反码除符号位外,其他位按位取反 -  原码
	//得到c真正的值:-126
	
	
	printf("%d\n", c);
	return 0;
}

C语言中比int长度小的只要char和short,所以只有这两种类型包括它们的有无符号类型。

测试题

再来几个测试题,看看你是否真的懂了。
1.

int main()
{
	char a = 0xb6;
	short b = 0xb600;
	int c = 0xb6000000;
	if (a == 0xb6)
		printf("a");
		
	if (b == 0xb600)
		printf("b");
		
	if (c == 0xb6000000)
		printf("c");
	return 0;
}

答案为

c

在这里插入图片描述
解析

0xb6是十六进制,b 是11,二进制:1011; 6的二进制:0110。所以0xb6二进制:00000000 00000000 00000000 10110110 - 32位整型
赋值给char类型发生截断 - 10110110 - char是有符号类型,此为负数。
执行运算a == 0xb6 时发生整型提升 提升后仍为负数,与0xb6不相等
同理,short类型也是如此过程,int类型直接赋值,不用整型提升,所以结果相等

int main()
{
	char c = 1;
	printf("%u\n", sizeof(c));
	printf("%u\n", sizeof(+c));
	printf("%u\n", sizeof(-c));
	return 0;
}

在这里插入图片描述
与上题类似,运算时发生整型提升。

算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。

long double
double
float
unsigned long int
long int
unsigned int
int

从下往上转换

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。

int main()
{
	int a = 4;
	float f = 4.5f;
	float r = a + f;

	return 0;
}

警告: 但是算术转换要合理,要不然会有一些潜在的问题。

二、类型的基本分类

类型的意义:

  1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
  2. 如何看待内存空间的视角。

整形家族:

char
    unsigned char
    signed char
short
    unsigned short [int]
    signed short [int]
int
	unsigned int
	signed int
long
	unsigned long [int]
	signed long [int]

浮点数家族:

float
double

构造类型:

数组类型
结构体类型 struct
枚举类型 enum
联合类型 union

指针类型:

int *pi;
char *pc;
float* pf;
void* pv;

空类型:

void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型。

关于整型如何存储,请点击跳转
对于整形来说:数据存放内存中其实存放的是补码。

为什么呢?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理; 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

#include<stdio.h>
int main()
{
	int a = 2;
	int b = -1;
	//b:
	//10000000000000000000000000000001 - 原码
	//11111111111111111111111111111110 - 反码
	//11111111111111111111111111111111 - 补码
	//16进制为 ff ff ff ff 16进制的1位相当于二进制的4位
	return 0;
}

正整数a在内存中的存储:
在这里插入图片描述
负整数b在内存中的存储:
在这里插入图片描述

三、大小端字节序

什么大端小端:

大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。

在这里插入图片描述

为什么有大端和小端:

对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。

如何判断当前字节序

  1. 调试观察内存即可

在这里插入图片描述

  1. 写一个小程序判断
int check_sys()
{
	int i = 1;
	//16进制 00 00 00 01
	//我们只需要知道它存储在内存中的第一个字节,就可以判断。
	//1、如果是小端存储 低地址处的第一个字节位01  -低地址  01 00 00 00  高地址
	//2、如果是大端存储 低地址处的第一个字节位00  -低地址  00 00 00 01  高地址
	//怎样只取第一个字节呢?
	//可以使用指针,什么类型的指针就只能访问该类型大小的空间
	//char类型指针只能访问一个字节

	char* p = (char*)&i;//将i的地址强转并赋值给p

	return *p;//对p解引用,访问第一个字节,然后返回其取到的值
}


//精简
//int check_sys()
//{
//	int i = 1;
//	return *((char*)&i);
//}

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

	return 0;
}

在这里插入图片描述

四、浮点型在内存中的存储

如何表示

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

(-1)^S * M * 2^E
(-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
M表示有效数字,大于等于1,小于2。
2^E表示指数位。

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

IEEE 754规定: 对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
在这里插入图片描述

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

如何存储

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位,它的取值范围为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从内存中取出还可以再分成三种情况:

  1. E不全为0或不全为1

这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(1023),得到真实值,再将有效数字M前加上第一位的1。 比如: 0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则1.0*2^(-1),其阶码为-1+127=126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制表示形式为:

  1. E全为0

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

  1. E全为1

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

这时来看一个题目:

int main()
{
	int n = 9;
	float* pf = (float*)&n;
	printf("n的值为:%d\n", n);
	printf("*pf的值为:%f\n", *pf);
	*pf = 9.0;
	printf("n的值为:%d\n", n);
	printf("*pf的值为:%f\n", *pf);
	return 0;
}

在这里插入图片描述

解释:

  • n=9没问题

  • *pf=0.000000
    *pf是单精度浮点类型,它把内存看作上述的三份S、E、M。
    9的原码: 00000000 00000000 00000000 00001001
    对pf解引用后,把其当作浮点型取出,S=0,E=0,M是最后23位
    表示为(-1)0*0.00000000000000000001001*2-126=1.001*2-146
    是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000。

  • n的值为:1091567616
    对pf指向的空间赋值,9.0是一个浮点型
    打印的时候采用%d,以整数的形式打印
    9.0 - 1.001*23
    S =0 ,M=127+3=130,E=001
    存储在内存中为0 10000010 00100000000000000000000
    转换之后与答案相符
    在这里插入图片描述

  • 以%f打印浮点型没问题


总结

希望能对读者理解数据的存储有帮助,如有错误欢迎大佬指出。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

s_persist

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

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

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

打赏作者

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

抵扣说明:

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

余额充值