一.引言
相信很多同学在学习过计算机导论之后,已经知道整数在内存中是以补码的形式存储的。但是关于整数存储中更多的知识,以及浮点数在内存中的存储,可能还存有疑惑。本篇文章中将对整数和浮点数在内存中的存储作一个较为详细的讲解。
二.整数在内存中的存储
2.1 整数的原反补码
为了避免有的同学还不知道整数在内存中是以补码的形式存储的,我们这里再简单提一嘴
整数的二进制表示方法有三种,分别是原码,反码和补码
其中,正整数的原、反、补码都相同,而负整数的原码就是将十进制的整数转换成二进制之后的形式,将原码取反(0变1,1变0,符号位不变)之后得到反码,反码+1得到补码
这三种二进制表示方法中,最高位被视为符号位,符号位是0则为正,符号位是1则为负,剩余位都是数值位
整型为什么在内存中以补码的形式来存储呢?原因在于,使用补码可以将符号位和数值位统一处理,并且因为cpu只有加法器,原码转换成补码的时候是取反+1,补码转换成原码的时候也是取反+1,相同的运算过程就不需要额外的硬件电路了
2.2 整数存储在char中
我们知道,char类型的大小为1个字节,也就是8bit,而int类型的大小是4个字节,也就是32bit。所以当我们将一个整数赋值给一个char类型的变量时,就会发生截断,也就是只会取整数的补码的后8个bit位存储进去。
另外的,不只是整型有signed(有符号)和unsigned(无符号)之分,char类型的变量也有signed char和unsigned char,signed char的范围是-128~127,而unsigned char因为没有了符号位,范围是0~255
我们这里先放一张图,以便更好理解接下来要讲的内容
图中,红色的是补码,蓝色的是实际数值
ned char类型的变量中,当我们将正整数赋值给变量时,此时补码和原码相同。如果我们不断的进行+1操作直到127的话,此时补码为01111111,然后再+1,就会进位变成10000000
但是:signed char的最高位是符号位,此时最高位是1,代表负整数。而负整数的原反补码不同,所以我们要对补码进行取反+1得到原码,再换成十进制才能得到真实数值。但是补码10000000是一个例外,按理来说将其换成十进制整数后不应该是-128,但是程序运行下它就是-128
我们再进行一次+1
会发现变化的规律和上面的圆盘图是一样的
另外的,在unsigned char中,没有符号位了,变化规律如下:
因为char类型只占8个bit,所以当值为255也就是补码为11111111时,此时再进1就会变成100000000,但是因为只能取8个bit,就会变成00000000,也就是0
所以当我们运行以下代码的时候:
#include<stdio.h>
unsigned char c = 0;
int main()
{
for (c = 0; c <= 255; c++)
{
printf("hello world\n");
}
return 0;
}
会发生什么呢?
当c为255时,再+1后又变回了0,所以造成了死循环
三.大小端字节序和字节序判断
我们在调试程序的时候,可以打开内存窗口观察内存细节
我们会发现,当0x11223344在内存中存储的时候是以字节为单位倒着存储的,这是为什么呢?
因为我们的电脑使用的是小端存储模式,这里就要引入大小端字节序的概念了。
3.1 大小端字节序
其实超过一个字节的数据在内存中存储的时候,就有存储顺序的问题。按照不同的存储顺序,我们分为大端字节序存储和小端字节序存储
- 大端(存储)模式:数据的低位字节内容保存在内存的高地址处,数据的高位字节内容保存在内存的低地址处。
- 小端(存储)模式:数据的低位字节内容保存在内存的低地址处,数据的高位字节内容保存在内存的高地址处。
上面的代码中,“44”是低位字节内容,而内存窗口中从左向右内存由低到高,所以“44”在左边的低地址处,“11”是高位字节内容,所以放在右边的高地址处,这就是小端存储模式
3.2 大小端字节序的由来
为什么有大小端字节序之分呢?
因为计算机电路先处理低位字节的话效率会更高,因为计算都是从低位开始的,所以小端存储模式被广泛应用于现代CPU内部存储数据。并且使用小端存储模式的时候不移动字节也能改变变量占内存的大小,例如我想将4字节的int32类型转换成8字节的int64整型,就只需要在末尾补0即可。
但是人类的阅读习惯更加适合大端字节序,所以除了计算机的内部处理,其他的场合(如网络传输和文件存储)几乎都是大端字节序
3.3 大小端字节序判断
“请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)”
这是一道来自百度的笔试题,大家可以自己尝试动手做一下,代码放在下面
#include <stdio.h>
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;
}
01 00 00 00
函数返回:1
00 00 00 01
函数返回:0
四.浮点数在内存中的存储
观察这一段代码,明明n和*pf是同一个数,为什么打印出的整数和浮点数会相差这么大呢?
要理解这个结果,我们需要搞清楚浮点数在内存中的存储方法
首先我们得明确一个点:浮点数转换为二进制后,小数点后第一位的权重是,第二位是,以此类推
根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式
- 表示符号位,当S=0时,V为正数;当S=1时,V为负数
- M表示有效数字,并且M的范围为 [ 1 , 2 )
- 表示指数位
光这么说不好理解,我们举几个例子:
十进制的 5.0 ,写成二进制就是 101.0,相当于 1.01 * 2^2
那么按照上面的格式,可以得出S=0,蓝色部分就是M=1.01,红色部分就是E=2
IEEE 754规定:
对于32位的浮点数,最高的一位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位浮点数,最高的一位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M
4.1 浮点数存储的过程
IEEE 754对有效数字M和指数E,还有一些特别的规定
前面说过M的范围是 [ 1 , 2 ) ,也就是说在计算机内部保存M的时候默认第一位总是整数部分的1,因此可以被省去,只保存后面的小数部分。比如保存1.505的时候,可以只保存505,等到读取的时候再加上第一位的1,这样做就可以多保存一位有效数字。
至于指数E,我们要知道首先E是一个无符号整数,也就是unsigned int,这意味着如果E占8个bit,它的取值范围就是0~255;如果E占11个bit,那么它的取值范围就是0~2047。但是我们知道科学计数法中的指数是存在负数的可能的,所以IEEE 754规定,把指数E存入内存时必须再加上一个中间数,也就是把指数E的范围分成两半。对于8位的指数E,这个中间数是127;对于11位的指数E,这个中间数就是1023。比如2^10,指数E就是10,所以在保存成32位浮点数的时候必须保存成10+127=137,也就是10001001
4.2 浮点数读取的过程
指数E从内存中读取的时候还可以分为以下三种情况
1.E不全为0或不全为1
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
比如:0.5的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其阶码为-1+127(中间值)=126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制表示形式为:
0 01111110 00000000000000000000000
2.E全为0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
0 00000000 00100000000000000000000
3.E全为1
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位)
0 11111111 00010000000000000000000
完.