大端、小端存储方式

首先我们来看这样一个简单的例子:

int main()
{
	int a = 16;
	return 0;
}

看到这个例子,你有没有曾经疑惑过,这个16在内存中就是是怎么放的呢?于是我打开调试窗口,发现a的地址在内存中对应的方式是这样的:

alt

现在我们想想,16对应的16进制数字应该是0x00 00 00 10(一个整型4个字节,对应8个16进制数),那为什么内存中不是 00 00 00 10,而是倒转过来10 00 00 00呢?

说到这就不得不引入大端存储模式小端存储模式

大端存储模式:指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中

小端存储模式:指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中

注意,这两种存放方式都是以字节为单位存储数据,为了更好理解,我画了下面这个图:

alt

这就是分别对应着大小端不同的两种存储方式下数据的存储情况,值得注意的是,一般的电脑采用的存储方式是小端存储,正如我前面图所示

有了存储方式的概念,下面我们引入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;
}

所以我们接下来打开内存的监视窗口,观察内存是否如我们如我们所想

alt

结果如我们所想。而且我们还可以通过查看下面的头文件内容去查看int类型的存储范围:

<limits.h>

当我们打开上面的头文件后,可以看到int类型和unsigned int 类型的存储范围:

alt

为什么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

下面是测试的结果:

alt

回到一开始的问题,为什么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那是不是这样呢?

alt

这结果竟然和我们想的不同,那问题究竟出在了哪里呢?

下面我们先来介绍一下浮点型数据的存储方式

首先我们引入一个标准: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对应的内存布局如下:

alt

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。现在我们打开调试窗口,看看内存情况:

alt

有了上面的知识储备,现在让我们回到最初的问题:

#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

alt

最后我们再来看看char类型的存储方式:

char型数据的存储方式

开始前先看个序章,看看下面的这个简单的程序:

#include<stdio.h>

int main()
{
	char a = 'a';
	return 0;
}

你有没有考虑过,这个字符a是怎么放的呢?实际上,由 ' ' 括起来的字符,实际存入char中是以该字符对应的ASCII码值来存放的,不信我们测试一下,a的ASCII码值是97 alt 我们打开内存调试窗口看看:alt。16进制的61对应的正好是10进制下的97。

有了上面的认识,我们打开limits.h的头文件看看:

alt

其实char类型的变量也有有符号和无符号之分,我们都知道,char类型的变量占8个比特位,对应着就是从0 ~ 255,无符号的char类型自然是0 ~ 0xff。有符号的就是-128 ~ 127,那为什么是这样规定的呢?我们看看char类型的内存补码

alt

这里我从网上看到一个很好的解释,首先我们可以看到,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);
}

上面的程序会输出什么呢?我们看看:

alt

下面我们来分析一下上面的例子:

首先是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的身份看就不同了,直接原码就是补码就是反码

alt

对于b,它再内存中的补码和a一样是1111,1110,因为b是无符号整型,所以它没有符号位,故它参与整型提升时补0。

补码、反码、原码:0000,0000,0000,0000,0000,0000,1111,1110

所以%d=%u=254

alt

我们可以总结一下整型提升的一些细节,即补码前面补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

alt

但是有没有更方便的记忆方法呢?下面有一个更方便记忆的方法:

alt

用上面的图猜测:

#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);
}

结果:

alt

第一次写这么长的博客,写完还是挺开心的,可能里面会有很多错误或者理解不到位的地方,恳请大家批评指正,继续加油!