数据的存储
大端、小端存储方式
首先我们来看这样一个简单的例子:
int main()
{
int a = 16;
return 0;
}
看到这个例子,你有没有曾经疑惑过,这个16在内存中就是是怎么放的呢?于是我打开调试窗口,发现a的地址在内存中对应的方式是这样的:
现在我们想想,16对应的16进制数字应该是0x00 00 00 10(一个整型4个字节,对应8个16进制数),那为什么内存中不是 00 00 00 10,而是倒转过来10 00 00 00呢?
说到这就不得不引入大端存储模式和小端存储模式
大端存储模式:指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中
小端存储模式:指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中
注意,这两种存放方式都是以字节为单位存储数据,为了更好理解,我画了下面这个图:
这就是分别对应着大小端不同的两种存储方式下数据的存储情况,值得注意的是,一般的电脑采用的存储方式是小端存储,正如我前面图所示
有了存储方式的概念,下面我们引入int类型的变量在内存中的存储方式
int类型数据的存储方式
首先我们知道,一个int类型的数据在内存中所占的空间大小是4个字节,也就是说对应着32个比特位,int类型的数据存储方式就是如上面例子所示,将32个比特位的数据转换作16进制数据存储在内存中,下面举几个例子:
int main()
{
int a = 20;//20对应16进制数是0x00 00 00 14
int b = 95;//95对应16进制数是0x00 00 00 5F
int c = 1888888;//1888888对应16进制数是0x00 1c d2 78
return 0;
}
所以我们接下来打开内存的监视窗口,观察内存是否如我们如我们所想
结果如我们所想。而且我们还可以通过查看下面的头文件内容去查看int类型的存储范围:
<limits.h>
当我们打开上面的头文件后,可以看到int类型和unsigned int 类型的存储范围:
为什么int类型和unsigned类型范围是这样的呢?要解决这个问题,首先我们得引入原码、反码、补码
我们一般定义的整型变量转化为的二进制数字就是其对应的原码,例如:整型9的原码就是
00000000 00000000 00000000 00001001
但是,数据在内存中是以补码的形式存储的,而由原码到补码的转换中间还需要经历一个反码。这3种类型的数据具体的转化规则是:
1.当数据是正数,正数规定原码、反码、补码都相同
2.当数据是负数,负数的原码、反码、补码转换规则是:原码符号位不变,其他位按位取反得到反码,反码+1就是补码,举个例子:
9的原码:00000000 00000000 00000000 00001001
9的反码:00000000 00000000 00000000 00001001
9的补码:00000000 00000000 00000000 00001001
-9的原码:10000000 00000000 00000000 00001001
-9的反码:11111111 11111111 11111111 11110110
-9的补码:11111111 11111111 11111111 11110111
让我们来测试一下:
首先9的补码对应16进制数字是:0x00 00 00 09
其次-9补码对应16进制数字是: 0xFF FF FF F7
下面是测试的结果:
回到一开始的问题,为什么int类型和unsigned int类型的数据大小会是这个范围呢?首先在int类型的原码中,因为int类型是有正负的,所以对应二进制原码中第一个位就是符号位,而unsigned类型没有符号位,自然它的取值范围就是0 ~ 11111111 11111111 11111111 11111111(即0xff ff ff ff、或者4294967295、0 ~ 2^32);而int类型则少一位(符号位),从2^31-1~2^31(从负数那里去掉0)
下面我们来看看浮点型数据的存储方式:
浮点型数据的存储方式
首先我们来看一个例子哈:
#include<stdio.h>
int main()
{
float a = 9.0;
int* pa = (int*)&a;
int b = 9;
float* pb = (float*)&b;
printf("%.1f\n",a);
printf("%d\n",*pa);
printf("\n");
printf("%d\n", b);
printf("%.1f\n", *pb);
return 0;
}
}
这个程序会输出什么呢?按道理来说浮点型数按整型打印,应该是舍去了小数吧,而整数按浮点型打印,应该给它增一些小数点后的零吧。那结果的话应该是:9.0,9,9,9.0那是不是这样呢?
这结果竟然和我们想的不同,那问题究竟出在了哪里呢?
下面我们先来介绍一下浮点型数据的存储方式
首先我们引入一个标准:IEEE 754,它的内容是对于任意一个二进制数V都可以表示为:(-1)^s * M * 2^E。
其中(-1)^s表示符号位,当s=0,V为正数,当s=1,V为负数。
M表示有效数字,M的范围大于等于1,小于2
2^E表示数位,2^E表示2^E,(因为2进制下满2进1)
先举几个例子说明一下二进制转换:
9.0的二进制表示形式是1001.0,可以改写为1.001* 2^3(类似于10进制下小数点向左多移动了3位就乘10^(3),二进制下2^3)
5.5的二进制表示为101.1,因为5.5=4+1+1/2即2^2+2^0+2^(-1),对应的二进制就是101.1,那么它也可以改写成(-1)^0 * 1.011 * 2^2
-9.75的二进制表示为 -1001.11,即2^3+2^0+2^(-1)+2^(-2),那么可以改写成(-1)^1 * 1.00111 * 2^3
那么现在让我们来看看标准怎么具体规定的:
M--规定既然M是位于1到2之间,即M可以写成1.xx的形式,那么为了充分利用空间,在保存M的时候可以去掉前面的1,直接保存0.xxxxx后面小数部分的xxxxxx
E--规定指数E是一个无符号整数,但是指数E允许为负,于是存放时,真实的E值+127=存放的E值(即-2对应125,9对应136)
S、E、M对应的内存布局如下:
E取出时有一些注意事项:
1.E不为全0/不为全1;E直接减去127得到真实值,而M加上1,S不变即可
2.E全为0;E=1-127=-126,此时M不加1,还原为0.xxxx,用这种方法来表示无限小的数字
3.E全为1;有效数字M全为0,直接表示无限大
先举个例子来说明浮点型数据的存储方式:
int main()
{
float a = -1119;
return 0;
}
下面我们来看看a的内存存储方式,首先我们先估计一下:-1119可以改写成二进制数-10001011111,这个数的标准形式是(-1)^1+1.0001011111 * 2^(10),那么S=1,M=0001011111,E=127+10=137=10001001,那么我们就可以得到-1119的在内存中的数字了--1100,0100,1000,1011,1110,0000,0000,0000-- 对应的16进制数字是C4,8B,E0,00,根据小端存储的原则,在内存中布局应该是00,E0,8B,C4。现在我们打开调试窗口,看看内存情况:
有了上面的知识储备,现在让我们回到最初的问题:
#include<stdio.h>
int main()
{
float a = 9.0;
int* pa = (int*)&a;
int b = 9;
float* pb = (float*)&b;
printf("%.1f\n",a);
printf("%d\n",*pa);
printf("\n");
printf("%d\n", b);
printf("%.1f\n", *pb);
return 0;
}
首先我们分析一下:
整型9内存中的二进制表示为0000,0000,0000,0000,0000,0000,0000,1001
浮点数9内存中的二进制表示为0100,0001,0001,0000,0000,0000,0000,0000
首先来看a,a是浮点数,那么以float的方式看9.0那自然是9.0,然后将a强制转换为以int类型的数据输出,我们由上面可以知道浮点数9.0的存储方式,注意:强制类型转换不会改变数据的存储方式,只会改变读取的方式。我们不妨算一下上面的数字0100,0001,0001,0000,0000,0000,0000,0000,毫无疑问就是1091567616。
然后我们来看b,以整型的方式来看,9不用说就知道是9,那么以float的方式来看呢?第一个0被解释为符号位,但是中间属于E的空间的8个比特位都是0,那么毫无疑问根据E全为0的情况,这个浮点数就是无限小0,下面我们再来估计输出就没问题了:
9.0
1091467616
9
0.0
最后我们再来看看char类型的存储方式:
char型数据的存储方式
开始前先看个序章,看看下面的这个简单的程序:
#include<stdio.h>
int main()
{
char a = 'a';
return 0;
}
你有没有考虑过,这个字符a是怎么放的呢?实际上,由 ' ' 括起来的字符,实际存入char中是以该字符对应的ASCII码值来存放的,不信我们测试一下,a的ASCII码值是97 我们打开内存调试窗口看看:。16进制的61对应的正好是10进制下的97。
有了上面的认识,我们打开limits.h的头文件看看:
其实char类型的变量也有有符号和无符号之分,我们都知道,char类型的变量占8个比特位,对应着就是从0 ~ 255,无符号的char类型自然是0 ~ 0xff。有符号的就是-128 ~ 127,那为什么是这样规定的呢?我们看看char类型的内存补码
这里我从网上看到一个很好的解释,首先我们可以看到,char类型的变量是占8个比特位,那么有符号位下应该有2^7 * 2=256个取值,从自然数方向来看毫无疑问就是0 ~ 127,那么从负数方向看呢?理应是-127 ~ -0,但是这个-0,在内存中对应的补码就是1000,0000。同时-128的补码1,1000,0000在前8位相同,所以这个-0其实就是-128,所以负数方向取值就是-128 ~ -127,所以有符号的char类型取值就是-128 ~ 127。
整型提升
为了更好的解释下面的内容,下面引入关于整型提升的概念:
C语言中字符和短整型的算术运算,先对补码进行整型提升,再参与运算。一般会整型提升的地方包括if语句的判断、%d、%u等输出各式、一般四则运算都会发生整型提升。在进行整型提升时,符号位补齐,如果没有符号位,则补0
举个例子:
#include<stdio.h>
int main()
{
char a = -2;
unsigned char b = -2;
printf("a=%d b=%d\n", a, b);
printf("a=%u,b=%u\n", a, b);
}
上面的程序会输出什么呢?我们看看:
下面我们来分析一下上面的例子:
首先是a,它在内存中的存放的补码是1111,1110,当它参与运算进行整型提升,得到的结果是:
补码:1111,1111,1111,1111,1111,1111,1111,1110
反码:1111,1111,1111,1111,1111,1111,1111,1101
原码:1000,0000,0000,0000,0000,0000,0000,0010
那么以%d的身份看自然就是-2,那要是以%u的身份看就不同了,直接原码就是补码就是反码
对于b,它再内存中的补码和a一样是1111,1110,因为b是无符号整型,所以它没有符号位,故它参与整型提升时补0。
补码、反码、原码:0000,0000,0000,0000,0000,0000,1111,1110
所以%d=%u=254
我们可以总结一下整型提升的一些细节,即补码前面补0还是符号位是与它的类型有关,而不是与它输出时是有符号输出%d还是无符号输出%u有关。
下面再回到char类型数据存储的问题上,我们可以看到,char类型数据的范围是-128 ~ 127,那么当存储大于这个范围的数据会怎样呢?
int main()
{
char a = -354;
char b = 2217;
printf("%d\n", a);
printf("%d\n", b);
}
上面的程序a=-98,b=-87,那么这些数字是怎么来的呢?下面来分析分析:
首先-354的原码是:1000,0000,0000,0000,0000,0001,0110,0010
反码:1111,1111,1111,1111,1111,1110,1001,1101
补码:1111,1111,1111,1111,1111,1110,1001,1110
所以它存入a中的是1001,1101,当它以%d形式打印时,发生整型提升:
补码:1111,1111,1111,1111,1111,1111,1001,1110
反码:1111,1111,1111,1111,1111,1111,1001,1101
原码:1000,0000,0000,0000,0000,0000,0110,0010
所以输出就是-98了,类似的也可以求出2217为-87
但是有没有更方便的记忆方法呢?下面有一个更方便记忆的方法:
用上面的图猜测:
#include<stdio.h>
int main()
{
char a = -354;//-354=-98-256=-98
char b = 2217;//-87+256*9=-87
char c = 128;//127+1=-128
char d = 166;//127+39=-128+38=-90
char e = -178;//-128-50=78
printf("%d %d %d %d %d", a, b, c, d, e);
}
结果:
第一次写这么长的博客,写完还是挺开心的,可能里面会有很多错误或者理解不到位的地方,恳请大家批评指正,继续加油!