数据在计算机中的存储方式(二)——整数在计算机中的存储进阶篇

   

目录

   

导语:

一、C语言中和二进制的有关运算操作符的介绍

1.1:位操作符

1.1.1:& 按位“与”

1.1.2:| 按位“或”

1.1.3:^按位“异或”

1.2:移位运算符

1.2.1:左移运算符:

1.2.2:右移运算符:

二、整数参与运算过程中的整型提升: 

2.1:整型提升的的定义

2.2:整型提升的规则是什么

 2.3:验证计算机中整型提升确实存在

2.4:关于什么时候会发生整型提升的问题

2.5:整型提升和强制类型转换

三、整数类型在内存中存储顺序的问题:

3.1:数据中高位字节和低位字节:

3.2:认识内存(一)——理论层面:

3.3:认识内存(二)——从C语言的角度

3.4:大端字节序存储和小端字节序存储

3.5:写一个代码来判断当前机器的存储模式

总结:


导语:

    前面在初阶篇我们一起探讨了整数在计算机中的二进制存储形式,我们知道整数是在计算机中以补码的二进制形式进行存储,以及为什么要以补码的形式而不是原码进行存储的原因。但是还有一些遗留的问题我们还没有解决:比如我们C语言中和二进制有关的一些运算还并没有给大家进行介绍,不同整数类型在进行运算时的一些细节问题还没有进行探讨,以及整数二进制在内存中存储顺序的问题。

    现在我们来一个一个解决吧!

一、C语言中和二进制的有关运算操作符的介绍

    C语言中能直接对存储在计算机内部整数二进制形式进行操作的运算大体分为以下两大类:位操作符和移位运算符:

1.1:位操作符

位操作符又被细分为以下三类,现以表格的形式呈现如下:

位操作符的运算规则一览
&(按位“与”)对应二进制位上,有0则为0,同时为1才为1;
|(按位“或”)对应二进制位上,有1则为1,同时为0才为0;
^(按位“异或”)对应二进制位上,相同为0,相异为1;

注意:位操作符的操作对象只能是整数!

1.1.1:& 按位“与”

对于&(按位“与”),我们可以类比我们中文中的“并且”,“和”等等,A和B为真,当且仅当A和B同时为真,只要其中一个为假结果就为假。C语言中用数字1表示真,0表示假,于是会有上述的运算规则。

我们来一起计算一个例子:

eg:-5 & 3= ?

我们首先要有一个意识,这些二进制的操作符都是在数据补码形式基础上进行的,这个很重要,因为初学者很容易犯的一个错误就是拿一个数的原码去直接进行计算了,而这分析出的结果和运行结果往往是有差异的。于是我们会这样去做,先写出这些数的补码:

-5的补码:11111111 11111111 11111111 11111011

 3的补码:00000000 00000000 00000000 00000011

-5&3结果:00000000 00000000 00000000 00000011

这个结果符号位为0,是正数,补码和原码相同,于是-5 & 3= 3。

应用:

基于按位“与”的运算规则,它有一个很重要的应用场景:任何一个整数和1进行按位”与“操作,可以得到它二进制位的最后一位!

1.1.2:| 按位“或”

对于|(按位“或”),我们可以类比我们中文中的“或者”,“或”等等,A或B为真,当且仅当A或者B中有一个为真就可以了,只有A和B同时为假结果才为假,于是就是有上述的运算规则。

我们来一起计算一个例子:

eg:-5 | 3 = ?

我们先把数的补码拿过来:

-5的补码:11111111 11111111 11111111 11111011

 3的补码:00000000 00000000 00000000 00000011

-5 | 3结果:11111111 11111111 11111111 11111011

这个结果符号位为1,是负数,补码和原码需要进行转换,我们来写一下它的原码:

补码换原码:10000000 00000000 00000000 00000101

而这个结果是-5。

      总结和应用:

  1. 一个整数和0进行按位“或”会得到这个整数本身。
  2. 观察上述例子中按位“与”和按位“或”的结果,不难发现:正数&负数=正数。正数|负数=负数
1.1.3:^按位“异或”

按位“异或”,我们可以认为就是在判断两个数是否不同,如果不同就输出为1,反之则输出为0。

我们还是直接把上述例子拿过来吧:

eg:-5 ^ 3= ?

-5的补码:11111111 11111111 11111111 11111011

 3的补码:00000000 00000000 00000000 00000011

-5^3结果:11111111 11111111 11111111 11111000

这个结果符号位为1,是负数,补码和原码需要进行转换,我们来写一下它的原码:

补码转原码:10000000 00000000 00000000 00001000

于是-5 ^ 3= -8。

应用:

基于按位“异或”的运算规则,它有一些很重要的应用场景:

  1. 一个整数和它本身进行异或会得到0;
  2. 一个整数和0进行异或会得到这个数本身。

基于此,就能够实现两个整数变量值的交换,而不创建中间变量。

1.2:移位运算符

移位运算符有两个,一个是左移运算符,一个是右移运算符。两个运算符计算规则大体一致,细微之处略有差异。

其次和位运算符一样,它的操作对象只能是整数!

我们现在分别就这两个运算符进行介绍:

1.2.1:左移运算符:

左移运算符使用的基本格式是:

a<<n;

其中a是我们的移位运算符的操作对象。n表示我要向左边移动的位数,也就是移动了多少位的意思。

左移运算符的运算规则是:

整数二进制位向左移动一位,缺失位用0进行补位。

举个栗子(●'◡'●)

以整数13作为例子,如果我要去分析13<<3=?,先写出13的原码:

13的原码:00000000 00000000 00000000 00001101

13是正数,原反补相同,所以13的原码也就是13的补码。这里我们再强调一下:C语言有关的二进制操作符都是在数据补码形式基础上进行的,所以一定要先写出这个数的补码再进行分析:

13的补码:00000000 00000000 00000000 00001101

左移三位嘛,左边的三个0就移除出去了,应该会出现这样的结果:

13<<3之后:00000000 00000000 00000000 01101

13作为int类型有32个bit位,也就是32个二进制位,缺失位用0进行补充:

所以最后的结果应该是这样的:

13<<3的实际结果:00000000 00000000 00000000 01101000

这个结果符号位为0,是正数,原码和补码相同,因此结果就是104。不知道大家发现了没有:

13<<3=13*2^3=13*8=104。这也是左移运算符的内核本质:

原先没有左移之前:00001101=1*2^0+1*2^2+1*2^3=13

       左移三位之后:01101000=1*2^3+1*2^5+1*2^6=2^3(1*2^0+1*2^2+1*2^3)=13*8=104.

总结:

对于左移运算符,如果变量a左移n位则:a<<n=a * 2^n.

1.2.2:右移运算符:

首先右移运算符使用的基本格式是:

a>>n

其中a是我们的移位运算符的操作对象。n表示我要向右边移动的位数。

其次右移运算符相比左移运算符更加复杂一些,基于不同编译器在处理右移运算符的差异,我们将右移运算符分为以下两种:一种是算数右移,一种是逻辑右移。现在分别进行介绍:

算数右移:

对于算数右移而言,它的运算规则是:

右边抛弃,左边补符号位——即如果这个数是负数,符号位原本是1,我们就补1。反之如果这个数是正数,符号位原本是0,我们就补0。

以整数13和-13为例子:

分别去分析计算13>>1的结果和-13>>1的结果

              13的补码:00000000 00000000 00000000 00001101

算数右移后的结果:00000000 00000000 00000000 00000110

这是正数,补码和原码相同,于是对于算数右移:13>>1=6。

             -13的补码:11111111 11111111 11111111 11110011

算数右移后的结果:11111111 11111111 11111111 11111001

这个结果的原码是:10000000 00000000 00000000 00000111

                                             于是对于算数右移:-13>>1=-7。

逻辑右移:

对于逻辑运算而言,它的运算规则是:

右边抛弃,左边补0,而不考虑这个数的正负性。

同样的例子:

分别去分析计算13>>1的结果和-13>>1的结果

              13的补码:00000000 00000000 00000000 00001101

逻辑右移后的结果:00000000 00000000 00000000 00000110

这是正数,补码和原码相同,于是对于算数右移:13>>1=6。

             -13的补码:11111111 11111111 11111111 11110011

逻辑右移后的结果:01111111 11111111 11111111 11111001

符号位为0,是正数,补码和原码相同,而这是个非常大的数字。

同时绝大多数主流的编译器,像VS 2022, Dev-c++, GCC等等都只支持算数右移。因此我们的重点放在算数右移上面就可以了,当然啦!也不是说逻辑右移是错误的,算数右移才是正确的。这仅仅只是不同的处理器在处理数据时的差异而已,只是一般算数右移能更加贴合于我们日常生产生活的需要而已,仅此而已。

最后还有一个问题,n的值我们的例子用的是正数,可不可以是负数呢?如果负数对于移位运算符有意义的话,我可不可以认为:a>>n是将a右移n位,那么a>>-n是左移n位呢?

我们还是来编程测试一下吧!

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>

int main()
{
	int a = 13;
	//13的补码:00000000 00000000 00000000 00001101
	//右移一位:00000000 00000000 00000000 00000110--6
	//左移一位:00000000 00000000 00000000 00011010--26
	printf("%d\n", a>>1);
	printf("%d\n", a << -1);
	//如果a>>-1表示的含义是左移一位的话,这个结果应该是26!
	return 0;
}

这是在VS 2022下的测试结果:

结果并不是我们所想的那样,而且你把Visual Studio编译器后面的警告拿过来看一下:

实际上,n是负数的这个应用场景是我们C语言标准是没有定义的,我们日常生活中也不要这么去写,虽然程序也能跑,但是运行结果是我们无法去分析预测,也是没有意义的。

     总结:

  1. 对于正数,它的算数右移和逻辑右移的结果是一致的;
  2. 对于负数,算数右移的结果仍然是负数,逻辑右移的结果往往是一个很大的正数;
  3. 绝大多数的编译器使用的是算数右移的运算逻辑,我们平常所说右移也是默认为算数右移;
  4. 左移一位有乘二的效果,右移一位偶数会有除二的效果,但是奇数则需要进行具体分析。

二、整数参与运算过程中的整型提升: 

C语言有很多很多的数据类型,比如精度很高的long, long long类型,精度比较小的char,short类型,以及int类型,但是它们直接可以直接进行运算,其中的底层逻辑是怎样的呢?我们一起来了解一下。

2.1:整型提升的的定义

C语言的算数运算总是以缺省整型类型的精度来进行的,所以在处理这些类型的时候,比如说算数表达式中字符类型和短整型在使用之前,常常会先转换为普通整型再进行计算,这种转换我们称之为整型提升。

另外其实我们日常用到的CPU整型运算器(简称:ALU)的操作数的字节长度,一般就是int类型的字节长度,也就是32比特位,也就是4Byte的大小,同时这也是CPU内通用寄存器的长度。因此即使是两个char类型的数据相加,也会先转换为CPU整型运算器的操作数的标准长度,再进行计算,以此也能够提高char类型数据运算时结果的计算精度。

用一句比较通俗的话来描述的话:你虽然只需要1Byte的字节,但是我一次只能拿出4Byte的空间啊,我还多出来了3Byte的空间,这些空间我也是送给你的啊,你用了这些空间之后,还能够提高你计算时的计算精度,这不用白不用嘛。所以计算机在设计时也就是用的这么一个道理。

2.2:整型提升的规则是什么

整型提升时分为两种情况:

一种是有符号的字符类型或短整型,即signed char与signed short类型。它们在进行整型提升时:


如果这个数是正数,则高位补0:

举个例子:char a = 1:

计算机中a的二进制序列是:00000001

整型提升之后a的二进制序列是:00000000 00000000 00000000 00000001


反之这个数是负数,则高位补1:

举个例子:char a=-1;

计算机中a的二进制序列是:11111111

整型提升之后a的二进制序列是:11111111 11111111 11111111 11111111
另一种是无符号的字符类型或短整型,即unsigned char与unsigned short类型,
它们在进行整型提升时:
无论这个数是正数也好,负数也罢,高位都是补0。

 2.3:验证计算机中整型提升确实存在

理论终究是死板的,这里我们将带着大家通过一些代码来验证我们计算机中整型提升的实例场景:

代码一:

#include<stdio.h>

int main()
{
	char a = 0x8b;
	short b = 0x8b00;
	int c = 0x8b000000;

//如果a的值没有发生改变的话,就输出a
	if (a == 0x8b)
		printf("a ");

//如果b的值没有发生改变的话,就输出b
	if (b == 0x8b00)
		printf("b ");

//如果c的值没有发生改变的话,就输出c
	if (c == 0x8b000000)
		printf("c ");
	return 0;
}

运行结果:

 解析:

首先C语言中以0x开头的数据表示该数据以十六进制形式进行展示,同时一个十六进制位等价于4个二进制位,因此可以根据一个数的十六进制位直接写出它的二进制位。现在的问题是:根据一个数的十六进制位直接写出的这个二进制序列是它的补码形式还是原码形式呢,答案是补码。我们可以通过下面这个代码来验证我们的想法:

#include<stdio.h>

int main()
{
	int a = 0x80000001;
	//变量a的二进制序列:
	//10000000 00000000 00000000 00000001
	//对变量a的二进制序列进行取反加一:
	//11111111 11111111 11111111 11111111
	printf("%d", a);
	//如果a的原二进制序列是它的原码,则会输出-1的结果;
	//反之a的原二进制序列是它的补码,则会输出-2147483647的结果。
	return 0;
}

运行结果:

然后接下来,对于char类型的变量a而言,a=0x8b=10001011,Visual Studio 2022环境下char类型默认为signed char类型,同时符号位为1,认为是负数,因此整型提升时高位补1,这个时候的变量a和原来的变量a明显是不相等了;

对于short类型的变量b而言,b=0x8b00=10001011 00000000,接下来的分析步骤和结果同char类型的变量a相同;

而对于int类型的变量c而言,c=0x8b000000=10001011 00000000 00000000 00000000,c变量作为一个int类型的变量它不需要进行整型提升这种转换的过程,因此变量c的值会和最开始的值保持一致;

代码二:

#include<stdio.h>

int main()
{
	char c = 1;
	char b = 1;

	//这里的c作为char类型的变量没有参与除sizeof以外的其他的算数运算,不会发生整型提升
	printf("%zd\n", sizeof(c));

	//这里的c除了参与sizeof的算数运算,还参与了自增运算,会发生整型提升
	printf("%zd\n", sizeof(+c));

	//这里的c除了参与sizeof的算数运算,还参与了自减运算,会发生整型提升
	printf("%zd\n", sizeof(-c));

	//这里的c除了参与sizeof的算数运算,还和同类型的变量b进行了加法运算,b和c都会发生整型提升
	printf("%zd\n", sizeof(b + c));

	//整型提升的效果只存在于参与算数运算的过程,这个效果不会延续,因此不会影响后面变量c的性质
	printf("%zd\n", sizeof(c));
	return 0;
}

运行结果:

代码三:

#include<stdio.h>

int main()
{
	char a = -1;
	signed char b = -1;
	unsigned char c = -1;
	//1.首先这三个数据在内存都是以补码形式存储都是:11111111的形式.
	//说明:变量c是个unsigned char类型,原则上不能放负数进去,但是没关系,因为放进去的本质上是二进制数码嘛

	//2.其次%d是以十进制形式打印有符号整型,什么是整型? int类型嘛,因此这些变量打印过程中都会发生整型提升
	
	printf("%d\n", a);
	//3.VS 2022默认char类型是有符号的,有符号char类型变量,整型提升时高位补符号位,于是变量a会变成:
	//11111111 11111111 11111111 11111111
	//这是补码,原码形式是:
	//10000000 00000000 00000000 00000001:%d的角度来观察变量a,以有符号的角度去分析二进制序列,这个值就是-1

	printf("%d\n", b);
	//4.分析过程同变量a的分析过程

	printf("%d\n", c);
	//5.这个是无符号char类型变量,整型提升时高位补0,所以整型提升之后变量c的二进制序列为:
	//00000000 00000000 00000000 11111111
	//符号位是0,原码和补码相同,从%d的角度,即有符号角度去分析二进制序列,结果就是255.

	return 0;
}

补充:

注释中提到了%d格式是以十进制形式打印有符号整型,和它一个比较像的是%u格式,%u是以十进制形式打印无符号整型。

代码四:

#include<stdio.h>

int main()
{
	char a = -128;
	//-128写二进制,直接从char类型角度不好操作,可以先从整型角度写出它的二进制序列:
	//-128作为int类型的二进制补码:
	//11111111 11111111 11111111 10000000
	//这样的一个二进制序列要存进一个char类型的变量,只存一个字节,也就只拿低位的8个bit位
	//于是变量c的补码:10000000

	char b = 128;
	//我们在初阶篇了解到,一个有符号的字符类型,它的取值范围为-128到127之间
	//因此值128显然超过char类型能表示的最大值,能不能存进去呢?答案是可以的!
	//同前面的道理一样,先把128当作int类型写出它的补码:
	//00000000 00000000 00000000 10000000
	//在存储的时候也只拿8个字节,于是变量b的补码:10000000

	//由于两个数存进去的二进制序列都是10000000,且类型相同,因此整型提升之后结果也会保持一致
    //注:类型相同也是一个很重要的条件,因为有符号类型和无符号类型在整型提升时的规则是有差异的!
	if (a == b)
	{
	//注:char或short类型在进行大于,小于,等于等这样的大小关系判断时,也会发生整型提升哦!
		printf("Yes");
	}
	else
	{
		printf("No");
	}
	//所以最后程序的输出结果就是Yes。
	return 0;
    //读者朋友们拿到我的代码运行之后,请做个小测试,修改a,b变量中其中一个的类型为unsigned char类型,再测试一下运行结果。
}

到这里,我想对于初阶篇遗留下问题:补码形式为10000000直接就等于-128的原因,大家应该已经有了自己的体会和理解,因为它作为int类型时补码的形式就是那么一回事,我们只不过拿到了它最低位的8个二进制位用了一下!

可能有些小伙伴会说:可是128补码也是10000000的形式啊!但是我们同时要注意到char是个有符号类型的哦,最高位也就是符号位为1表示的含义应该是负数,所以它首先应该是个负数才对!

当然如果char作为一个无符号类型时10000000,它就表示128,没有问题!

当然了代码的最后,大家一定要记得修改变量类型接着测试一下啦!这样这个代码前后的底层逻辑你应该就能彻底清楚啦!

2.4:关于什么时候会发生整型提升的问题

分析这个问题,我个人认为应该从两个角度去分析:

第一个角度是类型的角度:能进行整型提升的类型一定是精度低于int类型的数据,比如我们的char和short类型,也主要是这两个类型哦!

其次,我们还要去看它要做什么事情,整型提升存在的意义就是为了提高精度低于int类型数据在计算时结果的计算精度,但有时候我们会发现这样做又是没有必要的,甚至可能会取得适得其反的效果,比如我要用sizeof运算符去计算它的字节长度的时候,所以这种时候你会发现编译器并没有进行整型提升。

但是其他的情况,比如加减乘除啦,自增自减运算啦,要进行数据大小关系的比较和判断啦,以及函数形参部分是int,你传递了char或者short类型的变量啦,还有一个char类型或者short类型的值赋值给到一个int类型的变量。这些时候都会有整型提升的发生。

2.5:整型提升和强制类型转换

作为初学者比较容易将整型提升强制类型转换搞混在一起,但是实际上这两者之间有着很明显的区别:

  1. 首先整型提升是一种现象,而强制类型转换是一个操作符;
  2. 整型提升只发生在整型之间,强制类型转化则可以发生在所有类型之中;
  3. 整型提升是自发性的,强制类型转换则需要程序员自己使用强制类型转换符()才会发生;
  4. 整型提升只能是低位向高位进行,eg:char赋值给了int类型变量会发生,int赋值给了char类型变量则无事发生,甚至会有精度的丢失!而强制类型转换则既可以发生在低位向高位过程,也可以发生在高位向低位的过程中,甚至是浮点型和整型这样的不同类型之间。

总之差异还是挺明显的,希望大家不要搞混哦!

三、整数类型在内存中存储顺序的问题:

我们现在学习了很多存储单元,按照从小到大的顺序列举出来就是:比特(bit),字节(Byte:常缩写为B,千字节(kB),兆字节(MB),吉字节(GB)等等,对于一个内存而言,它的最小存储单元不是比特,而是字节。因此我们这里所说的整数在内存中的存储顺序也只是针对内存大于1个字节的数据类型才有意义。

当然大家也可以认为就是除char类型以外的其他类型,因为它是我们在C语言阶段所能接触到的字节为1字节的数据类型,其他的数据类型无一不例外都是大于一个字节的

在深入讨论之前,我们先来熟悉一些概念:

3.1:数据中高位字节和低位字节:

这个概念对于绝大多数读者可能会比较陌生,但是不要担心,我们来一步一步认识这个东西。

首先是高位和低位的概念:

什么是是数据的高位和低位呢?我们来看一个例子:

二进制数据:10001101,如果我现在要你把这个数转换成十进制你会怎么做?我想你应该会这么去做:

要算这东西很简单对不对!但是我希望大家看到一个东西,就是2的指数,这个东西啊,更专业的称呼叫做“权重”,你会发现啊,我们在计算的时候从左到右,它是不断变大的,也就是权重是在不断增大的!

于是我们会把权重较小的二进制位叫做二进制数的低位,而把权重较大位置的二进制位相对得就称之为高位。

于是就有了高低位的概念啦!

然后我们把字节的概念加进来:

前面的分析中,我们是单独去认识每一个二进制位的,但是如果整体去看二进制序列10001101,它就是八个比特位,恰好就是一个字节!如果现在在它的左边还有一个8位的二进制序列,那么这些序列在更靠左的位置,当然了,权重相应的也就会更大。于是我们就把原来由10001101组成的这个二进制序列,整体权重相对较低就被称之为低位字节序列,而靠左的,整体权重相对较高的二进制序列就被称之为高位字节序列


字节序的说明:

无论是高位字节序,还是低位字节序,它们都是字节序。所谓的字节序就是以字节为单位的进制序列。比如8位二进制位是字节序,2位十六进制位也是字节序。只要大小为1个字节的进制序列就称之为字节序。

3.2:认识内存(一)——理论层面:

内存嘛!很简单,通俗来说就是存放数据的容器,这应该没有问题。

接下来要说的是:内存是以字节为单位去进行存放数据的。也就是说:单位内存空间的存储容量是一个字节!为什么这么说呢?就拿我们平时在用手机场景时来说明吧:当你在下载某个文件的时候,或者你去查看手机内存的时候,你会看到GB, kB, B这样的存储单位符号,但是我相信你绝对不会说看到了bit这样的存储单元符号吧!

然后我们平时使用的设备,比如手机,它的总内存都是很大很大的了,现在最小也基本都是128GB总内存空间起步,这是什么概念呢?相当于这台设备中就有着:128*1024*1024*1024块内存空间,不同的内存单元里面放的东西是不一样的,那我们的计算机又是根据什么样的原理去管理这么大一块内存空间的,从而保证了它能根据我们的需求准确无误而且非常迅速地定位一块内存单元,并且拿出对应内存中的数据的呢?

答案当然是:编号!就像我们不同的学生有不同的学号,不同的员工有不同的员工编号,在计算机中,用一个地址的编号来唯一指向我们计算机内部一块内存存储单元。如果我们以后要表示这块内存单元的话,就用这个地址编号来进行唯一的表示。它们之间就形成了一个一一对应的关系!

3.3:认识内存(二)——从C语言的角度

我们的C语言里面有一个操作符,这个操作符就是&——取地址操作符,可以用它来直接获取到一个变量的地址编号,学编程的人一般都习惯把这个地址编号叫做地址,所以&就直接叫它取地址操作符,就不叫它取地址编号操作符啦,前者叫起来也更顺口一些。

然后取出来的地址,我要存起来我可以使用指针变量。

但是不知道大家有没有这么一个疑问:对于一个char类型的变量,它只需要1个字节的空间就可以了,所以我要对一个char类型的变量进行取地址的操作,这一点问题也没有。但是我们很多时候取地址的对象都是大于一个字节的,比如说2个字节的short类型变量,4个字节的int类型变量……

两个字节大小的变量就需要两块内存单元来存放它,于是和这个变量相关的地址编号就有两个;

四个字节大小的变量就需要四块内存单元来存放它,于是和这个变量相关的地址编号就有四个……

取地址操作符只取出一个地址啊,那计算机是如何处理这种情况的呢?

碰到这种棘手的问题,我们一般都是可以编程来做个小测试来看一下的(同时为了让大家更清楚看到和认识C语言中变量内存的布局格式,我们给变量赋值了十六进制数据):

然后我们打开VS的调试功能,并且打开其中的内存窗口,来看一下两个变量的内存布局是怎样的,来做个对比:

注:读者朋友们如果用的也是Visual Studio的编译器,可以在自己的电脑上面测试一下这个程序,在测试的时候提供两个小小的建议:

  1. 请在调试期间打开控制台窗口,然后再进行参数的对比。这是因为程序重新运行的时候会给变量重新分配内存单元。不是同一个进程,即便变量名是一样的,存放数据的内存单元也已经不再是之前的内存单元了。这个时候进行的分析就没有意义了;
  2. 其二,在使用内存窗口查看当前变量的地址的时候,我们仍然可以使用C语言中的取地址操作符&,来直接获取到当前变量的地址。

这个图看起来可能会有些模糊,于是这里作者将其中重要的信息以及相应的解读给大家筛选出来,供大家进行参考:

根据上面的解读:我们不难总结出以下几点重要讯息:

  1. 每一个存放数据的地址编号下都放着一个十六进制序列,这个十六进制序列由两个十六进制位组成,也就是一个字节的大小,这和我们前面的所说单位内存空间的存储容量是一个字节是相吻合的。
  2. 其次地址编号也是有大小的,内存窗口中从上到下,地址编号由低到高;在同一行从左到右,地址编号也是由低到高:我们将地址编号较高的叫高地址,相对较低的我们叫做低地址。至此我们有了高低地址的概念。
  3. 有了第二点的铺垫,我们会发现:C语言在为局部变量分配内存空间的时候,习惯上会优先使用高地址处的内存空间。就比如a变量和b变量,a比b先创建,因此它的地址也会比b变量的地址更高。
  4. 最后,我们前面的疑惑也得到了解答:&变量名——如果变量存储空间的大小大于一个字节,那么它会将这个变量所有地址中最低地址的值拿给你。当然也有些地方说是将这个变量第一个字节的地址拿出来,这也是可以的,没有任何问题!

3.4:大端字节序存储和小端字节序存储

前面的学习,我们已经了解到了存储单元大于一个字节的数据类型有高位字节序和低位字节序的说法,内存也有高低地址之分。现在的问题是:高位字节序就一定要放到高地址处,低位字节序就一定要放在低地址处吗? 

答案是:不一定!在计算机CPU的寄存器问世的时候,由于寄存器的宽度一般是大于一个字节,于是就有过关于:如何将一个多字节序列存储到计算机内部的问题。

有些人主张将高位字节序存放到内存中的高地址处,而将低位字节序存放到内存的低地址处,这样就将权值和内存地址的高低有效地一一对应了起来。而另外一部分人则更加中意于将低位字节序存放到内存中的高地址处,而将高位字节序存放到内存的低地址处。

后来在计算机的演变和发展中,人们发现这两种存储字节序的方式都各自拥有对方无法替代的优势,于是这两种存储字节序的方式都被保留了下来,并给它们分别命名为小端字节序存储大端字节序存储。分别依靠彼此的优势而分别各自沿用至今,服务于不同的应用场景之中。现将它们各自的定义分别陈列如下:

  1. 小端字节序存储:是指将数据的高位字节序存储到内存中的高地址处,而将它的低位字节序存储到内存中的低地址处的这种存储数据的模式;
  2. 大端字节序存储:是指将数据的高位字节序存储到内存中的低地址处,而将它的低位字节序存储到内存中的高地址处的这种存储数据的模式;

根据彼此的定义:现在以short类型的数据0x1234为例子,来分别看一下两种存储模式在存储这个数据时的差异:

0x1234在小端存储模式和大端存储模式下存储

存储模式地址编号:0x00ff40地址编号:0x00ff41
小端存储模式0x340x12
大端存储模式0x120x34

根据定义去理解这个表格应该没有太大的问题,现在我的想法是:我定义了一个int类型的变量,并且通过强制类型转换的方式,将数据0x1234给到这个int类型的变量……当然啦,如果你很熟悉我们前面整型提升的知识了,直接赋值也是可以的,这没有任何问题。

那么这个变量的值应该是0x00001234(在保证值不变的前提下,将原先的16个字节变成了32个字节):

0x00001234在小端存储模式和大端存储模式下存储

存储模式0x00ff400x00ff410x00ff420x00ff43
小端存储模式0x340x120x000x00
大端存储模式0x000x000x120x34

你会发现这样的一个现象:

  • 对于大端字节序存储而言,它的符号位总是固定为内存中第一个字节的位置,大端字节序的这种存储特点,使得它在判断正负方面拥有得天独厚的优势;
  • 对于小端字节序存储而言,对比上面两个表格:你会注意到强制类型转换之后,它的字节顺序在内存中没有发生太大的变动,相比之下,大端字节序存储模式下,则发生了非常明显的变动。因此我们说小端字节序在强转之后不需要调整字节内容,不管你是1个,2个,还是4个字节的存储,它在内存中的相对位置都是一样的;
  • 其次小端存储模式下,CPU在进行数值运算时按照顺序从内存中从低位到高位截取数据,这样的运行方式会更加高效。

这是通过上面的这个例子,我们能直观看到的彼此的一些优势。看上去好像大端存储模式非常鸡肋,就一条明显的优势。但是大端存储模式在实际生产生活应用中相比小端更广泛。在各种网络通信,网络协议上面都会有它的身影哦!

3.5:写一个代码来判断当前机器的存储模式

我们把前面VS内存窗口的图片再看一下:

根据我们前面的理论知识以及图片所提供给我们的信息,我们不难知道当前我们的这个VS环境下,是小端字节序存储模式。但是,有些编译器可能没有像VS这么方便,能够很好去查看当前内存中的布局,这个时候你可以通过下面这段C语言代码来对当前机器的存储模式进行一个判断:

#include<stdio.h>

int main()
{
	int a = 1;
	//数字1的十六进制形式:0x00 00 00 01
	//小端字节序存储01 00 00 00
	//大端字节序存储00 00 00 01

	//因此我只要拿到变量a中第一个字节里面的内容就可以判断当前机器的存储模式:
	char* pc = (char*)&a;
	if (*pc == 1)
	{
		//如果拿出来的第一个字节序的内容是1,则说明:
		printf("当前机器是小端字节序存储模式");
	}
	else
	{
		//如果拿出来的第一个字节序的内容是0,则说明:
		printf("当前机器是大端字节序存储模式");
	}
	return 0;
}

总结:

这篇博客容量很大,而且和上一篇博客《数据在计算机中的存储方式(一)——整数在计算机中的存储初阶篇》之间间隔了较长的时间,这也是深感抱歉。可能也是博主最近也临近考试周,因此比较忙的缘故。

然后博客中有什么疑问,或者认为有不足的地方都欢迎大家在评论区进行留言,博主会在最迟6个小时内给大家一一回复。

接下来会快马加鞭进行这个系列第三篇博客《数据在计算机中的存储方式(三)——浮点数在计算机中的存储方式》的创作,还请大家继续支持啦~谢谢大家(鲜花)!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值