目录
一.前言
在我们学习编程的过程中,会遇到各种各样的数据类型,庄哥对此做出了总结:
注:(1).图中带‘ * ’表示C99添加
(2).字符类型也可以划分到整型类型中,因为字符是以ASCII码值的形式存入内存中的
那么在面对代码中杂而多的数据时,编译器又是如何将它们存储在内存中的呢?
这期博客,我们就来一起来探讨一下整型类型与浮点类型在内存中的存储方式吧!
二.整型类型在内存中的存储
2.1整型数据的二进制表示形式
整型数据的二进制表示形式有三种,分别是:原码、反码和补码
(1).原码:将十进制直接用二进制表示出来的结果
(2).反码:原码的符号位不变,其它位按位取反后的结果
(3).补码:反码加1后的结果
三者之间的转化关系:
其中有符号数与无符号数在内存中的二进制表示形式又不一样。
2.1.1无符号数
无符号数在内存中是以原码的形式存储的,即直接从十进制转化为二进制就可以存入内存中。
例如: unsigned int a=20在内存中存储的数值就是00000000000000000000000000010100,但是在我们调用内存窗口时,会发现编译器展现的数值是00 00 00 14(从高地址往低地址读)。这其实就是编译器将20的二进制转化为16进制来进行表达而已,主要是为了缩短长度,方便程序员观察。
对上述转化关系不明确的老铁可以看下图帮助理解
注:至于内存中的表示形式为何是14 00 00 00,这关乎后面的大小端问题,这里先留个悬念
2.1.2有符号数
(1).有符号数在内存中是以补码的形式存入内存的,即需要将原码取反后得到补码,再加1得到补码才能存入内存。
(2).这里需要注意有符号数的二进制形式的第一位(从左往右)为符号位,只影响数值的正负,不影响数值的大小
那么有符号数为什么不能像无符号数一样,用原码存入内存呢,这不是更方便吗?
其实不然,这里庄哥举个例子老铁们就知道了:
例如:char a=1,b=–1;
那么a+b=?
(1).如果用b的原码进行计算:b=10000001 a=00000001 a+b=10000010
最后结果转化为十进制后为–2,这显然是错误的。在上个世纪,这个问题也困扰了科学家很多年,但 是著名的数学家约翰·冯·诺伊曼,提出了补码这一概念,并且应用于计算机中,很好解决了这一难题。
(2).用b的补码进行计算:b=11111111 a=00000001 a+b=100000000
相加后的结果含有9个比特位,但是a和b都是char类型,超过了8个比特位。这时编译器就会自动截断 多余的部分,即这里的1被丢弃了,最后结果就为00000000,转化为十进制就为0
结论:补码的出现实现了符号位和数值域统一处理,同时让加法与减法也可以统一处理
讲清楚补码出现的原因后,这里庄哥还需要讲一下有符号数与无符号数在数值域上的区别。
注:这里以char类型举例,其它类型类似,有兴趣的老铁可自行推导
(1).无符号数
无符号数不含有符号位,所以它的数值域比较简单,从00000000 ~ 11111111,转换为十进制形式就是从0 ~ 255
(2).有符号数
有符号数含有符号位,所以它的数值域包含负数,二进制的范围同样是00000000 ~ 11111111。但这里的第一位(从左往右)是符号位,所以当符号位是1时,这时编译器就会将这个数转换为补码后,再存入内存中,而不是直接存入原码了,所以转换为十进制的数值范围为–128 ~ 127。
注:这里的10000000为补码,取反加1后得到原码100000000。这个原码二进制数总共有9位,但char类型只有8位,所以编译器会截掉多余的第一位(从左往右)。但是为什么最后的结果不是零,而是-128呢?其实是为了方便,前辈们就达成约定将char类型的有符号数10000000规定为-128,老铁们也需要记住哦!
2.2大、小端存储模式
(1).是什么:
- 大端存储:指数据的低位保存在内存的高地址,而数据的高位保存在内存的低地址
- 小端存储:指数据的高位保存在内存的高地址,而数组的低位保存在内存的低地址(大部分编译器采用)
(2).为什么:
这里引用百度上的一段话:这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。简而言之,大小端的出现就是为了解决:一个将数据按什么样的顺序排入内存的问题。
(3).怎么办:
那么我们如何通过代码来判断编译器采用的是哪种存储方式呢?
#include<stdio.h>
int check_sys()
{
int a = 1;
return (*(char*)&a);
}
int main()
{
int ret = 0;
ret = check_sys();
if (ret == 1)
{
printf("小端存储");
}
else
{
printf("大端存储");
}
}
VS编译器的运行结果是小端存储,在KEIL C51的环境中是大端存储。当然,我们还可以调用内存窗口来判断是哪种存储方式。
三.浮点型在内存中的存储
3.1浮点数的存储规则
浮点类型的存储方式与整型的存储方式相差甚大。根据国际标准IEEE754(电气和电子工程协会)的规定,C语言浮点数的存储规则如下:
( − 1 ) S ∗ M ∗ 2 E (-1)^S*M*2^E (−1)S∗M∗2E
- (–1)^S表示符号位,当S为0时,表示浮点数为正;当S为1时,表示浮点数为负
- M表示有效数字,1<=M<2
- 2^E表示指数位
其实可以类比成初中学过的科学计数法,只不过多了一项(–1)^S表示正负性罢了。其中S、M、E的值会转化为二进制存入内存中,如下图所示:
通过图示我们能直观的看出,在单精度浮点数存储中,S部分占用1个字节,E部分占用8个字节,M部分占用23个字节;而在双精度浮点数存储中,S部分占用1个字节,E部分占用11个字节,M部分占用52个字节
注:因为在内存中E是无符号数,所以不能表示负数。但是E用来表示指数位,是有可能出现负数的。为了指数位能够存入内存中,我们需要对指数位加上127,转换为正数后,再存入内存E部分中,下面举个例子。
例如:
整数5.0的二进制表示形式为101.0,按照公式即为: ( − 1 ) 0 ∗ 1.01 ∗ 2 2 (-1)^0*1.01*2^2 (−1)0∗1.01∗22
即S=0,M=1.01,E=2
E加上127后为129,129的二进制形式为:10000001
那么它的在内存中的存储形式为:0 10000001 01000000000000000000000
这时细心的老铁就会发现,5的二进制形式不是101吗,为什么在M部分中写成01,还有一个1呢?
其实,不论数据是单精度浮点数,还是双精度浮点数,M一定满足1<=M<2。所以为了能够表示更高的精度,整数部分1不在内存中存储,M部分只用来存储小数部分(如:浮点数1.xxx,只将xxx存入M中)。当数据从内存中取出时,编译器会自动加上1和小数点。
3.2 关于指数E的两种特殊情况
(1).当E全为为0时:
当E全为为0时,代表数据的指数位大小为–127。一个数的–127次方,将是一个特别特别小的数。这时,从内存取出数据时,编译器不再给M部分加上1和小数点,而改为0和小数点,以此来表示无穷小的数。
事实上,这时已经可以将这个数据的大小看作为0了,因为在十进制中,单精度浮点型只能表示小数点后6位,双精度浮点型只能表示小数点后15位。而–127次方已经远远超出它们所能够表示的精度范围了,这时将数据打印出来其实就是0了。
(2).当E全为1时:
当E全为1时,代表数据的指数位大小为128。一个数的128次方,将是一个特别特别大的数。这时,即便内存中M部分全部为0,还原成IEEE754规定的形式也是(–1) ^ S * 1.0 * 2 ^ 128,而2^128将是一个特别大的数,以此来表示一个 ± 无穷大的数。
注:由于这两种情况相当特殊,这里庄哥就不再举例说明了,老铁们也只需要知道即可。
3.3 关于小数点后不为0的情况
以上所举的例子都是表示,小数点后为0的情况,那么小数点后不为0,又该如何用二进制来表示呢?
这里就需要用到乘2取整法,就是将小数点后的数字不断乘上2,直到新数字的小数部分为0即可。然后,从上到下取整数部分,就可以表示小数部分的二进制形式。对于无法乘到小数部分为0的情况,就截断到第11个二进制位。
下面再举个例子:
3.3经典例题
请问下面这段代码的输出结果是什么呢?
#include<stdio.h>
int main()
{
int n = 9;
float* p = (float*)&n;
printf("%d\n", n);
printf("%f\n", *p);
*p = 9.0;//改变n的存储类型
printf("%d\n", n);
printf("%f\n", *p);
}
输出结果:
老铁们答对了吗,下面庄哥来解析这道题目
这道题的关键在于看待存储内容的方式与角度
(1).第一个printf打印类型是%d,而打印对象n在内存中的存储类型也是整型。以整型类型的形式存入内存,再以看待整型的方式取出,打印出9是毋庸置疑的。
(2).第二个printf打印的类型是%f,而打印对象为p。虽然p是float类型的指针,但是p所指向的变量是n。n为整型变量,所以n在内存中的存储类型为整型。由于打印类型为%f,所以是以看待浮点型的方式取出,最后打印结果就是0.000000。
(3).通过*p=9.0改变n变量的类型。第三个printf打印类型是%d,n的存储类型为float。以浮点型存入内存中,再以看待整型的方式取出,所以打印结果为1091567616。
(4).第四个printf打印的类型为%f,*p=9.0将n变量的类型改为float。以浮点类型存入内存,再以看待浮点型的方式取出,所以打印结果为9.000000也是毋庸置疑的。
四.结语
好了,本篇博客内容就到此结束。明白数据在内存中的存储形式,能更加直观、清楚地看待问题,对代码也能够有更深层次的理解,所以老铁们一定要理清楚各个数据类型在内存中是如何存储的。同时,因为庄哥也刚写博客没多久,尚且缺乏经验,所以欢迎老铁们能够指出本文不足之处,庄哥一定会听取老铁们的合理意见。