目录
一.数据的分类
整形:
char : unsigned char (char在内存中的存储形式为ASCII码值存储,所以归结为整形)
signed char
short : unsigned short
signed short
int : unsigned int
signed int
long : unsigned long
signed long
浮点数:
float
double
构造类型:
数组类型
结构体类型 struct
枚举类型 enum
联合类型 union
指针类型:
char* p1
int* p2
float* p3
double* p4
二.整形在内存中的存储
整形数据在内存中有三种表示形式为原码,反码,补码。
三种方法都有符号位和数值位两部分,符号位用0代表正数,用1代表负数,其余为数值位
正数的原码,反码,补码都是一样的
负数的原码,反码,补码各不相同
原码
将数值直接转换为二进制的形式就可以得到原码
反码
将原码(除了符号位外)中的其它位按位取反得到反码
补码
得到反码之后,反码+1就是补码
根据上图可以明显看出,整形数据在计算机种的存储为补码
为什么?
整数在计算机中存储为补码形式是因为补码具有一些重要的数学和计算属性,使其在计算机硬件中更易于处理和实现。以下是一些原因:
简化加法和减法:补码使加法和减法操作在硬件中变得更加简单,因为加法和减法可以使用相同的电路来执行。这减少了硬件复杂性和成本。
消除加法器中的进位处理:在补码中,正数和负数的加法可以像无符号整数一样进行,不需要额外的进位处理。这简化了加法器的设计。
唯一的零表示:在补码表示中,零有唯一的表示方式,这意味着不需要考虑正零和负零的区别。
便于硬件逻辑:补码的性质使得比较操作更加容易,因为可以使用相同的比较逻辑来比较正数和负数。
符号扩展:在补码中,符号扩展(将一个数扩展到更高位数,保持其符号不变)非常简单,因为只需在最高位上重复符号位。
数学一致性:补码在加法和减法操作中表现出与常规整数相同的数学属性。这使得数学运算在计算机中更容易理解和处理。
保留数字范围:补码允许表示更大范围的整数值,因为它使用所有位来表示数值,而不浪费一个位来表示正负号。
总之,补码是一种在计算机中表示整数的有效方式,因为它在硬件和数学运算方面提供了许多优势。这就是为什么大多数计算机体系结构选择使用补码来表示整数的原因。
int main()
{
int a = 5;
//原码 00000000 00000000 00000000 00000101
//反码 00000000 00000000 00000000 00000101
//补码 00000000 00000000 00000000 00000101
int b = -1;
//原码 10000000 00000000 00000000 00000001
//反码 11111111 11111111 11111111 11111110
//补码 11111111 11111111 11111111 11111111
int c = a + b;
//按照原码计算的话
//a 00000000 00000000 00000000 00000101
//b 10000000 00000000 00000000 00000001
//c 10000000 00000000 00000000 00000110 结果会是6
//但是按照补码计算的话
//a 00000000 00000000 00000000 00000101
//b 11111111 11111111 11111111 11111111
//c 100000000 00000000 00000000 00000100
//因为是32为的机器,所以1属于溢出,所以最终结果为00000000 00000000 00000000 00000100,转换为10进制为4
printf("%d", c);//4
return 0;
}
这样就简化了计算的方式,节省了开销。
这时又有一个问题,为什么上面数据在内存中显示的时候字节顺序不一样,好像是反过来的
这是为什么呢?
这就涉及到了大小端字节序存储的问题,别着急,往下看
三 .大小端字节序存储
大小端字节序(Big-endian和Little-endian)是一种计算机体系结构中的字节序(byte order)概念,用于描述在多字节数据类型(如整数和浮点数)存储在计算机内存中时,字节的排列顺序。这个概念的存在是由于不同计算机体系结构和处理器架构采用不同的字节序,而这会导致数据在不同系统之间的互操作问题。
什么是大小端字节序存储?
大端存储方式:数据的低位在内存中的高地址处存储,数据的高位在内存中的低地址处存储。
小端存储方式:数据的高位在内存中的高地址处存储,数据的低位在内存中的低地址处存储。
下面解释一下为什么存在大小端字节序:
多字节数据类型的存储方式:整数和浮点数通常由多个字节组成,例如4字节整数或8字节浮点数。这些字节需要以某种方式存储在计算机的内存中。问题在于,这些字节应该按照什么顺序存储,高位字节(most significant byte)在前还是低位字节(least significant byte)在前。
多种架构:不同的计算机体系结构和处理器架构采用了不同的字节序规则。这意味着在不同系统之间传输数据或共享数据时,必须考虑字节序的问题,否则可能导致数据解释错误。
网络通信:在网络通信中,数据在不同计算机之间传输,这些计算机可能使用不同的字节序。因此,网络通信协议通常定义了数据的字节序,以确保数据能够正确解释。
数据存储和文件格式:在将数据写入磁盘或从文件中读取数据时,也需要考虑字节序,否则可能会导致数据损坏或不可读。
因此,大小端字节序存储的概念存在是为了解决不同体系结构之间的数据互操作问题,确保数据可以在不同系统之间正确地传输、存储和解释。不同的处理器和计算机体系结构选择了不同的字节序规则,而程序员和系统设计者需要了解并考虑这些规则,以避免数据交互或存储时的问题。
比如 : int a = 0x11223344;
0x11223344中44代表数据的低位,11代表数据的高位
如果是大端字节序存储,则为:
如果是小端字节序存储,则为:
对于大家大部分的电脑,采取的应该都是小端字节序存储 。
四.整型提升
整型提升是指编程语言中的一种类型转换,其中编译器或解释器自动将一个数据类型转换为另一个数据类型,以便在表达式中进行计算或操作。这种转换是自动进行的,无需程序员明确指定。
表达式的整型运算要在 CPU 的相应运算器件内执行, CPU 内整型运算器 (ALU) 的操作数的字节长度一般就是 int 的字节长度,同时也是 CPU 的通用寄存器的长度。因此,即使两个 char 类型的相加,在 CPU 执行时实际上也要先转换为 CPU 内整型操作数的标准长度。通用 CPU ( general-purpose CPU )是难以直接实现两个 8 比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于 int 长度的整型值,都必须先转换为 int 或 unsigned int ,然后才能送入 CPU 去执行运算。
对于有符号数来说,整型提升时,根据符号位进行提升
如果符号位为1,则将剩余的位数用1补齐。
如果符号位为0,则将剩余的位数用0补齐。
对于无符号数来说,整型提升时,直接补0皆可
举例说明
例1
int main()
{
char a = -1;
//-1
//原码 10000000 00000000 00000000 00000001
//反码 11111111 11111111 11111111 11111110
//补码 11111111 11111111 11111111 11111111
//因为a为char类型,为8个字节,所以发生截断 a 为11111111
unsigned char b = -1;
//-1
//原码 10000000 00000000 00000000 00000001
//反码 11111111 11111111 11111111 11111110
//补码 11111111 11111111 11111111 11111111
//因为a为unsigned char类型,为8个字节,所以发生截断 a 为11111111
printf("a = %d b = %d", a, b);
//%d 以十进制的形式打印有符号整数(原码打印),所以会发生整型提升
//a 11111111
//因为a的最高位为1,所以用1补齐
//a 补码 11111111 11111111 11111111 11111111
// 反码 11111111 11111111 11111111 11111110
// 原码 10000000 00000000 00000000 00000001
//所以打印的数字为-1
//b 11111111
//因为b是无符号数,所以用0补齐
//b 补码 00000000 00000000 00000000 11111111
// 反码 00000000 00000000 00000000 11111111
// 原码 00000000 00000000 00000000 11111111
//因为最高位补齐后b的符号位为0,代表正数,所以原码,反码,补码都相同,
//所以打印的数字为255
return 0;
}
例2
int main()
{
char a = -128;
//a 原码 10000000 00000000 00000000 10000000
// 反码 11111111 11111111 11111111 01111111
// 补码 11111111 11111111 11111111 10000000
//因为a为char类型,为8个字节,所以发生截断 a 为10000000
printf("%u\n", a);
//%u 以十进制的形式打印无符号整数(原码打印),所以整型提升
//a 10000000
//a 补码 11111111 11111111 11111111 10000000
//因为%u 以十进制的形式打印无符号整数,所以它会将a当做正数对待
//所以打印的结果就是4294967168
return 0;
}
例3
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
看到这段代码,大家的第一反应可能会觉得很简单,直接就是打印256个hello world,但是这道题最容易出错的点就是在这里,此外我先给大家简单介绍char类型
char类型为1个字节,占8个比特位,这8个比特位可表示为:
有符号 char
同理 无符号char为
所以上面例3中,当i的值从0增加到255时,i++导致i的值变为0,这样就导致了for循环变成了死循环,会一直输出hello world
五.浮点数在内存中的存储方式
我们先看一道题
看到这道题的结果,是不是和大家的预期有点出入,有点难以理解,别着急,这就是因为浮点数和整型在内存中的存储模式不相同导致的,接下来我给大家介绍一下浮点数在内存中的存储方式。
根据国际标准 IEEE (电气和电子工程协会) 754 ,任意一个二进制浮点数 可以表示成下面的形式:(-1)^S * M * 2^E(-1)^S 表示符号位,当 S=0 , V 为正数;当 S=1 , V 为负数。M 表示有效数字,大于等于 1 ,小于 2 。2^E 表示指数位。
举一个例子float a = 101.0f;则101.1就可以表示为(-1)^ 0 *1.010*2^2则 S就是0,M则是1.010,E就是2
IEEE 754 规定:对于 32 位的浮点数,最高的 1 位是符号位 S ,接着的 8 位是指数 E ,剩下的 23 位为有效数字 M 。
对于 64 位的浮点数,最高的 1 位是符号位S,接着的 11 位是指数 E ,剩下的 52 位为有效数字 M 。
有效数字部分:IEEE 754 规定,在计算机内部保存 M 时,默认这个数的第一位总是 1 ,因此可以被舍去,只保存后面的xxxxxx部分。比如保存 1.01 的时候,只保存01 ,等到读取的时候,再把第一位的 1 加上去。这样做的目的,是节省 1 位有效数字。以 32 位浮点数为例,留给M 只有 23 位, 将第一位的1 舍去以后,等于可以保存 24 位有效数字。指数部分:首先, E 为一个无符号整数 这意味着,如果E 为 8 位,它的取值范围为 0~255 ;如果 E 为 11 位,它的取值范围为 0~2047 。但是,我们知道,科学计数法中的E 是可以出现负数的,比如:0.5 就可以表示为 1.0*2^(-1)所以IEEE 754 规定,存入内存时 E 的真实值必须再加上一个中间数,对于 8 位的 E ,这个中间数是127 ;对于 11 位的 E ,这个中间数是1023 。比如, 2^10 的 E 是 10 ,所以保存成 32 位浮点数时,必须保存成 10+127=137 ,即10001001。这样,指数部分在内存中取出就有三种情况: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 位 00000000000000000000000E 全为 0这时,浮点数的指数 E 等于 1-127 (或者 1-1023 )即为真实值,有效数字 M 不再加上第一位的 1 ,而是还原为 0.xxxxxx 的小数。这样做是为了表示 ±0 ,以及接近于0的很小的数字。E 全为 1这时,如果有效数字 M 全为 0 ,表示 ± 无穷大(正负取决于符号位 s )
掌握了这些知识,我们就可以理解上面那道题的解法了。
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就是00000000 00000000 00000000 00001001
把它当为浮点数就是0 00000000 00000000000000000001001
所以(-1)^0*0.00000000000000000001001 * 2^0 又因为以浮点数打印的时候精度为6,当n=9时,*pFloat结果就打印为0.000000
当*pFloat = 9.0,将n的值改为浮点数后,9.0就是1001.0。用浮点数表示为
(-1)^0*1.001*2^3 所以S=0,E=127+3=130,M=001
所以9在内存中的存储形式为0 10000010 00100000000000000000000 将这个数字转换为十进制就是1,091,567,616
第一个和最后一个打印的原因就是以整型的方式存进去以整型的方式读出来,和以浮点数的形式存进去,再以浮点数的形式读出来,结果是一样的,不会改变。
总结
这样,我们就清楚的了解到了整数和浮点数在内存中是如何存储的,以及它们的存储形式,此外,还了解到了整型提升以及大小端的知识,希望大家看到后能够对大家有所帮助,很高兴和大家一起分享!!!