导言:
本文章主要介绍以下几个方面:
1.整形在内存中的存储:原码、反码、补码
2. 大小端字节序介绍及判断
3. 浮点型在内存中的存储解析
正文:
一.整形在内存中的存储
变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
比如:
int a = 20;
int b = -10;
我们知道计算机为a 分配了四个字节的空间。那计算机到底是如何存储的呢?
首先我们先来了解原码、反码、补码的概念。
1.原码 反码 补码
原码、反码和补码是计算机中表示有符号整数的三种常见方式。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位正数的原、反、补码都相同。
负整数的三种表示方法各不相同。它们是为了解决整数的加减运算问题而设计的。
下面进行详细说明:
原码:
- 原码表示法是最直观的一种表示方法。一个整数的原码是用二进制数表示,最高位表示符号,其余位表示数值部分。
- 正数的原码与二进制表示一致,最高位为0,例如+7的原码为0000 0111。
- 负数的原码最高位为1,其余位表示该数的绝对值,例如-7的原码为1000 0111。
反码:
- 反码表示法是在原码的基础上取反得到的。正数的反码与原码一致,负数的反码是将原码中除符号位外的每一位取反。
- 反码中的0表示正数,1表示负数。例如+7的反码为0000 0111,-7的反码为1111 1000。
补码:
- 补码表示法是在反码的基础上加1得到的。正数的补码与原码一致,负数的补码是将原码中除符号位外的每一位取反,然后加1。
- 补码中的最高位为符号位,0表示正数,1表示负数。例如+7的补码为0000 0111,-7的补码为1111 1001。
三者的转化关系图:
在计算机中,计算机存储的都是二进制的补码。原因如下:
补码表示法解决了在计算机中进行加减运算的问题。在补码表示法下,一个正数的补码等于其原码,符号位为0;一个负数的补码等于其原码取反后加1,符号位为1。这使得计算机在进行加减运算时只需要进行简单的二进制运算,不需要额外的符号位处理。
另外,补码表示法还有一个特点:对于一个给定的位数,补码表示法可以表示更多的整数值,其中负数范围比原码和反码表示法更大一些。例如,8位补码可以表示-128到+127的整数范围,而8位原码和反码只能表示-127到+127的范围。
对于整形来说:数据存放内存中其实存放的也是补码。
看看编译器下的存储:
我们可以看到对于a和b分别存储的是补码。但是我们发现顺序倒了过来。这又涉及到了另一个知识点,大小端。
二.大小端介绍
大小端是一种描述数据在存储和传输中字节顺序的方式。字节顺序指的是多字节数据中字节的排列顺序,包括两种主要类型:大端序和小端序。
1.大端序:
- 在大端序中,多字节数据的高位字节存储在内存的较低地址,低位字节存储在内存的较高地址。
- 例如,十六进制数0x12345678在大端序中的存储顺序如下:
- 内存地址:低位字节 高位字节
- 内存值: 0x12 0x34 0x56 0x78
2.小端序:
- 在小端序中,多字节数据的低位字节存储在内存的较低地址,高位字节存储在内存的较高地址。
- 例如,十六进制数0x12345678在小端序中的存储顺序如下:
- 内存地址:低位字节 高位字节
- 内存值: 0x78 0x56 0x34 0x12
为什么存在大小端的区别?
大小端的存在主要是由于不同计算机架构和处理器的设计选择不同字节顺序所导致的。不同的系统和处理器可能使用不同的字节顺序,因此在进行数据传输和交互时需要考虑字节顺序的问题。
对于处理器架构而言,它们的寄存器、数据缓存、指令集等都使用一种字节顺序,这就要求在与外部设备交互时进行字节顺序的转换,以确保数据的正确解析和传输。
对于网络通信而言,由于不同计算机和操作系统之间的通信需要通过网络进行,因此在网络传输中也要考虑字节顺序的问题,以确保数据在不同平台之间的正确传递。
需要注意的是,大小端的应用领域相对较为底层,对于大多数应用开发者而言,并不需要直接关注大小端的问题,因为编程语言和相关库通常会处理好字节顺序的转换工作。只有在一些特殊情况下,如与底层硬件交互或进行网络通信时,才需要考虑大小端的影响。
三.浮点数存储规则
浮点数存储的例子:
#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("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
num 和*pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
浮点数在内存中的存储方式通常使用IEEE 754标准。根据IEEE 754标准,浮点数由三个部分组成:符号位、指数位和尾数位。
具体存储方式如下:
(-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对有效数字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为一个无符号整数(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从内存中取出还可以再分成三种情况:
非规格化数(Denormalized numbers):当指数位全为0时,表示浮点数为非规格化数。非规格化数的尾数位表示一个小于1的数,指数位的零值被解释为一个非常小的指数偏移量。这种情况下,浮点数的指数E被解释为一个负的偏移量,用于计算非规格化数的实际值。这时,浮点数的指数E等于1-127(或者1-1023)即为真实值。
规格化数(Normalized numbers):当指数位不全为0且不全为1时,表示浮点数为规格化数。规格化数的尾数位表示一个大于等于1且小于2的数,指数位的值被解释为一个偏移量。这种情况下,浮点数的指数E被解释为一个有符号的偏移量,用于计算规格化数的实际值。即指数E的计算值减去127(或1023),得到真实值,再将
有效数字M前加上第一位的1。特殊值(Special values):当指数位全为1时,表示浮点数为特殊值。特殊值包括正无穷大、负无穷大和非数值(NaN)。这种情况下,浮点数的指数E被解释为特殊值的标识符,而不是一个偏移量。这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)。
下面,让我们回到一开始的问题:为什么0x00000009 还原成浮点数,就成了0.000000 ?
首先,将0x00000009 拆分,得到第一位符号位s=0,后面8位的指数E=00000000 ,
最后23位的有效数字M=000 0000 0000 0000 0000 1001。
由于指数E全为0,所以符合上一节的第二种情况。因此,浮点数V就写成:
V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146)
显然,V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000。
总结:
编程的学习不仅仅要知其然还要知其所以然,一个合格的程序员得清楚的知道计算机背后的运行规则和方式,才能够在应对各种情况下有条不紊的解决问题。我也深深感觉到了计算机基础知识的重要性,学习也要往深处钻研,努力提升自己。