你知道整型数据在内存中是如何存储的吗?(新手小白必看)

在之前,我们讲解了有关C语言的一些基本知识,包括基本的数据类型,循环,判断,数组,结构体,指针等等,那么今天我们进入下一阶段的学习:C语言进阶。

在掌握了基本C语言的基础之后,相信大家现在对C语言的代码已经很熟悉了,也能写出一些不是很复杂的代码,那么这篇文章我就给大家带来一个容易被初学者忽视的问题,也是涉及到一些计算机底层的知识,那么话不多说,希望今天的讲解能够让大家对C语言有一个更深层次的理解。

1. 数据类型介绍

在之前,我们学习过C语言的一些基本的内置类型,包括如下几种:

char      // 字符数据类型
short     // 短整型
int       // 整型
long      // 较长整型
long long // 更长的整型
float     // 单精度浮点型
double    // 双精度浮点型

你应该掌握的是,每个内置数据类型占据几个字节,这个我就不赘述了,但有一点,其中的long数据类型的字节数无法确定,只能确定他的字节数是比int类型大的,但是具体指我们无法得知。

1.1 类型的基本归类

那么,我们学习了这么多数据类型,是不是应该给他们分一下类,以便于我们的使用呢。

整形家族:

char
    unsigned char
    signed char
short
    unsigned short [int]
    signed short [int]
int
    unsigned int
    signed int
long
    unsigned long [int]
    signed long [int]

注意:char类型也算在整型家族里。

那么有同学可能就要问了,char类型可是字符啊,怎么能算作整型呢?

这就不得不提到计算机的编码形式,我们知道,在计算机内部,数据都是以二进制的形式存储的,那字符型数据当然不例外,你能说计算机内部直接存储一个一个的字符吗,这显然不现实。

于是,我们发明了ASCII码表(这个大家都清楚),就是把字符型的数据转化成一个一个的整数,从而便于计算机的存储,所以字符型数据在计算机眼中也是一个整型,我们把它归类到整形家族中当然是正确的。

附:ACSII表

还有值得注意的一点是,在这里,我们引入了无符号数(unsigned)的概念,代表着这个数在计算机内部存储的时候,不需要符号位,所有的位数全部都是数值位,它们具体在计算机内部是如何存储的,我们之后会讲到。

接下来,我们来看浮点数家族:

浮点数家族:

float
double

哈哈哈,是不是感觉很孤单,只有float和double两个人,切记:浮点型家族内是没有无符号数的,也就是说没有什么所谓的无符号浮点数,这个说法是错误的,无符号数只是针对整型数据来说的。

构造类型:

> 数组类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union

数组类型我们都比较了解,举两个例子:

int arr1[10] = {0}; // 数组类型为 int[10]

int arrr2[5] = {1, 2, 3, 4, 5}; // 数组类型为 int[5]

结构体类型大家也比较熟悉了,如果忘记了,可以去翻看我之前写过有关结构体的博客。

至于剩下的两个类型,我们今天暂时不讲,之后会重点提到的。

指针类型:

int *pi;
char *pc;
float* pf;
void* pv;

指针类型大家应该是比较熟悉的,其中我重点说一下void*这个指针类型

void* 的这个指针类型代表的是指向的元素的数据类型是未知的,因此不能进行解引用操作,编译器会报警告。

那么有用学会问了,有好好的int*,float*,char*指针不用,我为什么偏要去用这个存在隐患的void* 类型的指针?

其实这个问题很好解释,在当我们不知道指针所指的数据类型是什么的时候,我们就应该使用void*类型的指针。举个例子:在相关人员进行程序开发的时候,定义函数形参的时候就应该使用void*,因为开发人员不知道用户调用函数之后,是如何使用这个指针的,可能有的用户将他指向了一个整数,那么他的类型就是int*,那也有可能有的用户将他指向一个字符,那么他的类型就是char*,所以通过void*参数的方式,减少了代码的复用,也更好的方便每一个用户。

2. 整型在内存中的存储

讲完了各种数据类型,下面我们来讲讲整型在计算机中到底是如何存储的。

在讲解存储方式之前,我们先来了解一下相关的一些概念。

2.1 原码 反码 补码

计算机中的整数共有三种表示形式:原码,反码,补码

三种方法均由符号位和数值位构成,符号位就是最高位,最高位如果为1表示负数,最高位如果为0表示正数。

规定:正数的原码 反码 补码 相同

但是负数就不一样了,负数的反码和补码是需要计算的

原码:直接将他的数值翻译成二进制就可以了,但要注意最高位为1

反码:符号位不变,数值位按位取反

补码:反码 + 1

那么,对于整型数据来说,其实在计算机中存储的是补码。

计算机为什么要引入反码和补码的概念呢?如何理解原码,反码和补码呢?下面讲一讲我的理解

2.1.1 原码

原码可以说是最简单的表示形式,最高位表示符号位,数值位存放二进制的绝对值

下面写出0 ~ 7和-0 ~ -7的原码

// 以四个比特位为例

    原码         原码
0   0000   -0    1000
1   0001   -1    1001
2   0010   -2    1010 
3   0011   -3    1011
4   0100   -4    1100
5   0101   -5    1010
6   0110   -6    1110
7   0111   -7    1111

用原码表示数据确实是简单,但是我们也能从上文中发现一些问题:

1. 0有两种表示方法,那么到底哪个用哪个呢?

2. 两个相反数相加(1 + (-1)= 1010 = -2)为什么不等于0?

于是我们发现,用原码表示数据,其实在正数相加的时候不会出现错误,但是在进行正数和负数相加减的时候哦会出现错误,这都是由于符号位引起的。

2.2.2 反码

因为在原码的表示形式下,两个相反数相加并不等于0,于是我们想,既然负数是一个正数的相反数,那么我们干脆用一个正数按位取反来表示负数得了,于是也有了反码的概念。

反码:正数的反码还是它本身,负数的反码是原码除了符号位之外按位取反得到的结果

// 以四个比特位为例

    原码   反码             原码    反码
0   0000   0000      -0    1000    1111
1   0001   0001      -1    1001    1110
2   0010   0010      -2    1010    1101
3   0011   0011      -3    1011    1100
4   0100   0100      -4    1100    1011
5   0101   0101      -5    1101    1010
6   0110   0110      -6    1110    1001
7   0111   0111      -7    1111    1000

这里说一下,由于计算机CPU内部只有加法器,而没有减法器,所以我们要讲减法转换成加法来计算,也就是加上这个数的相反数。 

我们用反码来解决一下原码出现的问题:

3 + -3 = 0011 + 1100 = 1111 = -0

-1 + -3 = 1010 = -5

哈哈,两个相反数相加的错误解决了,但是两个负数相加又不行了,而且数值0也是有两种表示方法,也是不符合要求的。

所以:反码表示在计算机中往往作为数码变换的中间环节,并不是进行整数运算的最终答案。

2.2.3 补码

补码的计算方法:反码 + 1

其实这并不是补码的定义,而是补码恰巧就等于反码 + 1,关于补码,我想讲一件它的由来和我对它的理解。

其实很简单,因为数据在内存中的存储都是有范围的,一旦超过那个范围,就会进行一个轮回。

接下来我举一个生活中的例子,时钟。比如说现在是10:00,我如果想让它变成8:00,那么该如何做呢?答案是将时针倒拨2个小时,或者正拨10个小时,两者的加时为12,这个12我们称作时钟的模。也就是说10 - 2 与 10 + 12是等效的,最后都等于8。

可能会有同学有疑问了,怎么10 + 12 会等于8呢,其实这是因为时钟的最大点数就是12也就是0,11:00之后的一个小时就会变成0:00,然后1:00,2:00,不存在什么13:00,14:00之类的。

那么在时钟中,减去一个数,就等于加上模再减去这个数,相信大家都能理解这个原理。

其实,我们引入反码,就是根据这个思想,把两个数相减变成一个数加上另外一个数的相反数来进行运算,只是没有处理好符号位,从而造成两个负数相加的时候结果错误。

在理解补码的原理之后,我们来看看如何计算一个整数的补码。

2.2.4 补码的计算方法

我们在讲补码的原理的时候说到,减去一个数等于加上一个数的同余数,这个数与减数相减刚好等于模。

我们以一个四位的二进制数举例,让我们计算6 - 2 = ?

四位的二进制数的模是多少?其实就是2 ^ 4 = 16

所以我们可以得出 6 - 2 = 6 + (16 - 2)= 6 + 14 

0110 - 0010 = 0110 + 1110 = 10100,6 -2 = 6 + 14 =  20

因为是四位二进制数,所以最多只能存4位,所以舍弃最高位,寄存器中存储的值为0100

我们可以看到 1110这个数代表14,那么我们是不是也可以将这个数看成 -2 的补码呢,这就是为什么负数的符号位是1而不是0的原因了吧。

由于-2的反码为1101 而补码为1110 所以才有了 补码 = 反码 + 1的计算方法(主要是方便计算)

这个时候,为了参与运算,我们将1110中的最高位1当作补码的符号位,其余的当作数值位。

到这里,关于原码,反码,补码的问题基本解决了。

解决了0有两种表示方法的问题,用补码进行运算可以保证正确率,这也是为什么计算机中存储的都是补码,而非原码,反码,因为这两个表示方法的局限性太强了。

2.3 大小端介绍

什么是大小端?

我们以整型举例,整型所占据的字节大小为四个字节,也就是说整型数据在内存中有四个字节的空间,那么这四个字节的空间是按照什么顺序排列的呢,是从小到大呢,还是从大到小呢,这就有了大小端的定义。

大端存储:是指将数据的低位存在内存中的高地址中,将数据的高位存在内存中的低地址中。

小端存储:是指将数据的高位存在内存中的高地址中,将数据的低位存在内存中的低地址中。

关于为什么会有大小端之分,其实很简单,上文我也说了,数据在内存中的存储不止一个字节,那么必然会有顺序的区分。

关于大小端,我们要知道的是其实是跟计算机内部构造有关的,而跟编译器的种类及其他因素是无关的。

那么,现在让你写一个函数来判断你的机器存储数据是大端还是小端,你该如何做呢?

代码如下:

int check()
{
	// 如果是大端,返回1,小端返回0
	int a = 1;
	return *((char*)&a);
}

int main()
{
	if (check()) printf("小端");
	else printf("大端");
}

现在让我来解释一下这个代码。

如图,是a的地址,因为a是一个整型变量,所以占四个字节

0x00000054608FF5A4 -> 01

0x00000054608FF5A5 -> 00

0x00000054608FF5A6 -> 00

0x00000054608FF5A7 -> 00

我们不难看出,低位的字节数据放到了低地址上,所以我们可以判断这个一个小端的存储方式。

好了,今天的有关整型数据的存储就到这里了,我们下篇文章会根据今天所讲的出一篇练习,从而巩固一下今天所学的知识。

那么今天的分享到这里就结束了,喜欢的小伙伴可以点赞加关注支持一波~

我们下期再见,拜拜!

你可能感兴趣的:(c++,开发语言)