IT技术之家

首页 > 后端

后端

整型数据是如何在内存中存储的_malloc不出对象

发布时间:2022-10-24 21:28:55 后端 0次 标签:c语言
数据在内存中的存储...

🏖?作者:@malloc不出对象
?专栏:《初识C语言》
👦个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐🙈🙈

目录

前言一. 整型在内存中的存储1.1 整型家族的介绍1.2 字符的本质1.3 原码、反码及补码的介绍1.4 大小端的介绍1.5 整型在内存中的存取规则总结1.5.1 例题 1.6 数据类型的取值范围1.6.1 例题


前言

本篇文章博主将给大家带来的是整型数据在内存中的存储的详细讲解,这块内容也是非常的重要并且经常容易出问题,不过请相信博主只要跟着我来一定让你明白的彻彻底底的🙈🙈

一. 整型在内存中的存储

我们之前讲过一个变量的创建是要在内存中开辟空间的,空间的大小是根据不同的类型而决定的。
那接下来我们就来谈谈数据在所在开辟内存中到底是如何进行存储的。

1.1 整型家族的介绍

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

从上面我们可以看到数据本身也分为有符号型(signed)和无符号型(unsigned),我们平常见的最多的还是有符号型,只不过它经常把signed省略掉了,例如:char == signed char
关于上述我还想问大家一个问题:为什么char字符型也被归于整型家族了呢? 接下来我想给大家好好谈谈字符。

1.2 字符的本质

用单引号括起的一个字符代表一个整数,我们也经常听到有人称之为字符常量,为什么它被称之为字符常量呢?
这是因为在C语言中字面字符都是以ASCII码的形式进行存储的,而ASCII码又是整数,并且每一个字面字符都有一个对应的ASCII码表示,因此字面字符其实是被看成int型的。
字符和整数没有本质的区别,可以给 char变量一个字符,也可以给它一个整数;反过来,可以给 int变量一个整数,也可以给它一个字符。
char定义的变量在内存中存储的是字符对应的 ASCII 码值。如果以 %c 输出,会根据 ASCII码表转换成对应的字符,如果以 %d 输出,那么还是整数。int 变量在内存中存储的是整数本身,如果以 %c 输出时,也会根据 ASCII码表转换成对应的字符。也就是说,ASCII 码表将整数和字符关联起来了。当然char类型占内存一个字节,signed char取值范围是-128~127unsigned char取值范围是0~255,我们也知道拓展的ASCII值也是最多到255,那么大于255的整数就不是字符了。

描述再准确一些,在char的取值范围内(0~255),字符和整数没有本质区别。

既然char的本质是整数,那C语言中为什么还需要char类型呢?
因为字符的个数不多,而char型变量占用的存储空间比int型变量小,所以用char型变量表示字符,为编程带来了方便;就算int型截断对最后的结果其实也是没有影响的,每一个字面字符都有一个对应的ASCII码与之对应。

下面我们来看一个例子来证明这个问题,大家想想下面会打印出什么结果?

#include<stdio.h>
int main()
{
	printf("%d\n", sizeof(1));
	printf("%d\n", sizeof("1"));
	printf("%d\n", sizeof('1'));
	char c = '1';
	printf("%d\n", sizeof(c));

	return 0;
}

我们一起来看看结果,结果显示4,2,4,1。下面我们来重点讲解最后两个结果。

printf("%d\n", sizeof(1));   //整型占4个字节没什么好说的
printf("%d\n", sizeof("1")); //字符串“1”,它包含俩个字符'1'和'\0',字符串以\0结尾,末尾会自动添加\0,为字符串结束的标志
printf("%d\n", sizeof('1')); //此时‘1’表示字面字符常量,‘1’ == 49,为int型占4个字节
char c = '1';                //那么这个地方其实也跟我们平时理解的有差别,其实我们的字符常量一直在进行截断,'1'为一个整型占4个字节,将它赋值给一个char类型的变量这里其实是隐式的发生了截断,由4字节截断为1字节,但只要我们的右值不超过char类型的数据范围对我们结果是不会产生影响的。                                     
printf("%d\n", sizeof(c));   //那么在此处既然'1'以经发生截断保存在char c当中了,而sizeof关注的只是数据类型,那么char就只占一个字节

C语言'1'被认为是一个整型占4字节,而在其他语言中可就不一定了,例如在C++‘1’是被看做1字节的:

总结:在C语言中我们的平常见到的字面字符其实都叫做字符常量,它占4个字节,只不过它用一个char(一字节)类型的变量来保存它;其中其实发生了隐式截断,但只要不超过char类型的最大取值范围是不会产生任何影响的,并且编译器也没有报错,就默认这种行为是可以的。我们在之后记住字符的本质就是整型就可以了。

1.3 原码、反码及补码的介绍

计算机中的整型有符号数有3种二进制的表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示正,用1表示负,而数值位用二进制位来存储。

计算机为什么只能认识二进制呢?
因为在计算机当中很多硬件和其他的一些方面是俩态性的,就比如说数据线只有正电和负电之分。
正数的原、反、补码都相同。
负数的原、反、补码就需要进行相应的转化了:

原码:直接将数值按照正负数的形式翻译成二进制就可以得到原码。
反码:原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码

对于整型来说:数据存放内存中其实存放的是补码,为什么呢?

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

我们的整型数据是以二进制的形式存储在内存当中的,那为什么我们在读取时要将补码转为原码呢?
因为我们人更擅长的是读取十进制数据。
那再问一个问题为什么人擅长使用十进制,因为人有十根手指头,在早期的我们使用手来进行计算,经过几百万年的发展,我们人就擅长使用十进制了🙈🙈

接下来我们通过一个例子来初步解释一下为什么整型数据在内存中的存储与浮点型数据在内存中的存储方式有区别

这是整型变量a中内存中表现形式:

这是浮点型f中在内存中的表现形式:

从上面俩幅图中我们可以知道整型a和浮点型f的在内存中十六进制序列不一样,这是不是说明了整型数和浮点数在内存中的存储方式不一样呢?接下来我们再来详细探究吧🙈🙈🙈

首先我们来计算一下a的补码 :4个字节,32个bit位,正数的原、反、补码是一样的
00000000 00000000 00000000 00001010 – 补码
地址是以十六进制序列来存储的,4个二进制位为1个十六进制位,在上图我们可以发现两个十六进制位连在一起组成为1个字节,整型a占4个字节所以总共有八个十六进制位。

那么我想问一个问题为什么地址要采用十六进制来表示呢?

1.计算机硬件是0101二进制的,16进制刚好是2的倍数,更容易表达一个命令或者数据。
2.二进制是在是太长了,容易看花眼,进制越大,数的表达长度也就越短,十六进制更简短,因为换算的时候一位16进制数可以顶4位2进制数,1111正好是F
3.那么为啥偏偏是16进制呢?
因为2、8、16,分别是2的1次方,3次方,4次方,这一点更加方便了进制之间的转换
4.最早规定ASCII字符集采用的就是8bit(后期扩展了,但是基础单位还是8bit),8bit用2个16进制直接就能表达出来,不管阅读还是存储都比其他进制要方便
5.计算机中CPU运算也是遵照ASCII字符集,以16、32、64的这样的方式在发展,因此数据交换的时候16进制也显得更好,但计算机最后操作的还是二进制。

回到上述得到的补码:0000 0000 0000 0000 0000 0000 0000 1010,我们将它化为十六进制的序列:0x00 00 00 0a ,我们对比上图整型变量a在内存中的表现形式,发现跟我们计算出来的得到的十六进制序列是相反的?下面我们就来介绍一下大小端。

1.4 大小端的介绍

什么是大端小端?

大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。

所谓的低位高位也被叫做低权值高权值,通俗的来讲也就是我们的个位十位百位,例如:12345,那么5就是最低位,1为最高位。

为什么会有大端和小端模式的存在呢?

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
通俗的来讲,大小端存储模式描述的是数据在内存中字节的存储顺序,而非二进制位的顺序。也称之为:大端字节序存储模式or小端字节序存储模式。

例如:我们吃鸡蛋,我们是从大端敲开还是从小端打开,先说明不能从中间打开啊🙈,这个问题没有一个统一的标准,因为每个人的习惯不同,所以意见没有达成一致!

下面我们来看看在VS下采用的是什么存储模式:

通过上图我们明显的看到在VS下我们采取的是小端存储模式,低位放在低地址处,高位放在高地址处。

整型a的十六进制补码形式为0x00000014,这里我们把它看做一个数,那么4是不是为个位,110位,这里的高低位就可以这样来解释。如上图我们可以看到00000014的高位存放在低地址中,而低位存放在高地址中,这种存放形式被称为大端存储模式;14000000的低位存放在低地址中,而高位存放在高地址中,这种存放形式被称为小端存储模式。那么第一幅图中它的存放顺序为14000000,这是不是证明了在VS编译器中采取的是小端存储模式呢。

1.5 整型在内存中的存取规则总结

首先我们来看一个例子,unsigned int b = -10;大家觉得它合理吗?

从编译结果来看确实没发现什么问题,那么我就有问题了我们说无符号类型不是只能表示正数吗?
但是此时右值为-10诶,为什么没出现任何警告和报错呢?下面我们来深入探讨一下整型是如何在内存中进行存储的,接下来请大家跟着我的思路一步步来。
大家看到unsigned int b = -10; 首先是不是对变量b进行了初始化操作,那么在之前的学习中我们也知道变量的本质是开辟一块空间,等待数据存放进来。无论是初始化还是赋值操作都是首先是要开辟一块空间,再将数据存放进来的,我们知道计算机只认识二进制,并且整型数据是以补码的形式存储在内存当中的。接下来我们就来进行转换一下:

1000 0000 0000 0000 0000 0000 0000 1010 - 原码
1111 1111 1111 1111 1111 1111 1111 0101 - 反码
1111 1111 1111 1111 1111 1111 1111 0110 - 补码

接着将补码存到变量b开辟好的一块空间,整型存储的时候空间是不关心内容的。数据存储的本质就是将二进制存储到对应开辟的空间,而此时在我们在将数据保存在空间内时,数据已经被转化成为了二进制,这就意味着我们的变量b只是起到了一个开辟多大空间的作用,在存储时关注的是-10的存储形式,而不关心b是什么类型的。

总结:整型数据在进行存储时,是不关心变量是什么类型的,它只是提供了一块空间,等待右边的数据以二进制的形式存储到空间就行了。

解决了这个问题,下面那么我们来看看变量b的类型到底起什么作用?

举个例子:现在我身上有100,请问我有多少钱?有人说你搁着跟我开玩笑呢,不就是100元嘛😏,我们的思维惯性是这样的,因为我们国家使用的是人民币,所以自然而然的说的100元。那么抛开我们是中国人这个身份,我的100可以是日元、美元、英镑、欧元…从这个角度来看100是没有任何意义的,必须要带上类型才有意义。100RMB交换100韩元你愿意换嘛?傻子才去换呢🤑

回到上面那个问题我们得到了-10的补码:1111 1111 1111 1111 1111 1111 1111 0110,那么你觉得它有意义嘛?它有多种类型啊,double flaot char unsigned char…那么读取出来的值又不一样了,所以我们的变量类型在这里就起作用了,它决定了价值。就像给100加了一个单位,如果你是100欧元的话那就比100RMB多的多了🙈🙈
unsigned int b = -10; 它是一个无符号整型,原码 = 反码 = 补码,那么b本质上为unsigned int型的话,那么此时补码 = 原码将会是一个很大的数,不过最后打印出什么还是取决于我们以什么角度来看待这个补码,为什么这么说呢?下面我们来看看:

当我们以%u无符号型的角度来看待这个问题的时候,此时补码=原码,打印出一个很大的数,而以%d有符号型角度来看待这个补码时,我们首先观察它的最高位符号位为1,那么证明此时为负数,既然为负数那么就要按照负数的转码形式进行转换将补码转成原码:

1111 1111 1111 1111 1111 1111 1111 0110 -补码
1111 1111 1111 1111 1111 1111 1111 0101 - 反码
1000 0000 0000 0000 0000 0000 0000 1010 - 原码

此时原码的最高位为符号位1,表示为负数,翻译为十进制就为-10了。

这个例子可能会有点难以理解,我们可以这样来想:你是男人,可是我就是要把你看成小姐姐啊,那在我眼里你就是小姐姐,本质上你还是个大帅哥;你是男人,那我也可以用正常的眼光看你就是男人啊,这两者之间的关系并不冲突。其实通俗的来讲:我们只是按自己的需求来实现罢了,你是一个无符号数,我想打印出有符号数,那就用%d打印啊。

下面我通过调试还给大家证明一下:

你发现什么?是不是我们的b实质上还是为无符号型不存在负数!!!

下面我来通过一张图总结一下上述过程:

好了,接下来再带大家看一个例子,本次我将完全按照上述的步骤来进行演示,大家先想想答案:

#include<stdio.h>

int main()
{
	char a = -10;
	printf("%d\n",a);
	printf("%u\n",a);
	
	return 0;
}

存:
1.变量a先开辟一块一字节的空间
2.讲-10的二进制补码写出来,1111 1111 1111 1111 1111 1111 1111 0110
3.讲其放入变量a开辟的空间内,我们发现此时变量a的空间容纳不了这么多bit位,此时就发生了截断,截取的是低八位1111 0110

取:
1.按照大小端的读取规则取出
2.看自身类型char,有符号型 观察最终打印出来的结果要不要进行整型提升,我们发现对于%d或者%u而言都要进行进行整型提升,此时变量a是有符号型,因此我们高位补的是符号位,这里符号位为1,所以发生整型提升之后补齐为1111 1111 1111 1111 1111 1111 1111 0110;
3.观察以什么角度打印出结果,%d打印先看符号位,符号位为1为负数按照负数的转码形式转换。
1111 1111 1111 1111 1111 1111 1111 0110 -补码
1000 0000 0000 0000 0000 0000 0000 1001 -反码
1000 0000 0000 0000 0000 0000 0000 1010 -原码
最终转为十进制为-10,
而以%u打印的话补码=原码,将会得出一个很大的数

整型在内存中的存取规则大总结:
存:
1.根据变量类型开辟一块多大的空间;
2.把右值的二进制补码写出来;
3.把其放到开辟好的空间内
取:
1.根据大小端的读取规则将补码读出;
2.看变量自身类型,再看需要打印的类型,如需要整型提升,则根据自身类型来进行判断,无符号型高位无脑补0,有符号型高位补符号位;
3.观察最终结果以什么角度打印出来,%d打印有符号十进制数,如符号位为0,则为正数补码=原码,如为1,则为负数根据负数的转码形式进行转换;%u打印无符号十进制数,补码=原码

注:以上规则在VS中一定适应,并且在VS中采用的是小端模式,所以在内存中我们看到它的倒着存进去的,而取出来的时候无论是大端还是小端都按照高低位顺序读取出来结果其实是一样的,如果读者还是不理解取时第一点的规则,可以忽略它,最终也是不会产生任何影响的。

相信大家如果能够完全理解我上述讲的规则,那么对于这部分相关的题都将迎刃而解。

1.5.1 例题

讲完整型在内存中的存取规则,下面我们来练几道题,关于这几道题大家下来可以好好练练,我就不做讲解了因为只要理解了我上述所说的规则,自己动手做一做绝对没什么问题的:
1.第一题

2.第二题

3.第三题

关于这题我做下说明:我们还是按照原来的方法把两者转为补码,再让补码相加运算,最后再根据取的规则打印结果。

1.6 数据类型的取值范围

C语言提供了许多的数据类型,在存储的时候分配的存储单元大小也不同,例如:char占一个字节,32位机器下int占4个字节,float占4个字节,double占8个字节…
这也就意味着不同的数据类型存在不同的取值范围,但它们都是以二进制补码的形式来进行存储的,下面我们就来详细谈谈各数据类型在内存中是任何分配的吧,这里以char为例因为它占最小的存储单元,取值范围是最小的,只要明白了它在内存中是如何分配的我们就可以推出其他数据类型的取值范围了。

关于上图中其实最难以理解的是负数的开端这个地方,接下来我想跟大家好好谈谈1000 0000.
以signed(有符号) char为例:其中最高bit位是符号位,意味着只有7个数值位。
原码:
1 1111111 -> -127,最小
0 1111111 -> 127,最大
那么signed char的取值范围不应该是[-127,127]吗?而事实上我们在很多资料上看到的signed char的取值范围是[-128,127],那么我们应该如何解释-128这个值呢?接下来坐稳了,我要发车了。

我们以unsigned型为例:
什么叫做取值范围?
假设我们有两个bit位,那么它的最小值为00 ,最大值为11,,总共有00 01 10 11这四种情况;那三个呢,最小值为000 ,最大值为111,总共有000 001 010 011 100 101 110 111这八种情况;
我们得出一个结论:所谓的特定数据类型,能表示多少个数据,取决于多个bit位对应的排列组合个数。

char占8个bit位就有2^8种排列组合的情况,对于计算机而言它绝对不会浪费任何一种排列组合或者少掉一种排列组合,因为计算机要用最小的成本来解决最大的数据计算问题。

回到上面那个问题
原码
1 1111111 -> -127 1 0000000 - > 1 1111111 [-0,-127]
0 1111111 -> 127 0 0000000 -> 0 1111111 [0,127]
我们发现竟然出现了两个0,-0也是0,而我们的0只能有一种标识方案,在选择时我们肯定选0。那么我们的char就少了这种排列组合的方式啊,就只有2^8 - 1种排列方式了,而计算机是不允许出现这种情况的,那么10000000到底标识谁呢?

我们的取值范围一定是连续的,所以只能出现两种情况:要么是-128,要么是128,那我们再看向符号位1,它表示为负数,所以最终1 0000000就用来标识-128了,所以signed char最终的取值范围为[-128,127]。

接下来我们再通过半计算的方式证明为什么是-128

这里为什么发生截断之后就无法正确的转换回来呢?这是因为存的时候空间不足,把一些数据丢弃掉了,所以导致发生“错误”无法转换回来,所以这才证明-128半计算半规定的一种方式。
这里如果大家有些地方听不懂的话那就直接记结论把🙈 事实上就是这么的巧妙,这些设计者也是通过严密的数学论证设计出来的。

博主也在网上查了很多资料,发现有些网上有些资料回答的太牵强了,例如:因为-0的补码1000 0000跟-128的补码相同???所以用-128来表示-0???这显然是不对的,从上图中我们也可以看出-128得到1000 0000是因为发生了截断所以才看起来与-0的补码一样,但它们两者的补码一定是不相同的。

解决了这个问题之后,我们接着来看看无符号char在内存中的分配,这样就很好理解了:

其实我们只要知道char类型在内存中的分配,其他数据类型的取值范围也是类似的,下面我直接给出结论:

signed char的取值范围 [-128,127] == [- 2^7,2^7 - 1]
unsigned char的取值范围 [0,255] => [0,2^8 - 1]
signed short的取值范围 [ -2 ^ 15,2 ^ 15 - 1]
unsigned short的取值范围 [0,2 ^ 16 - 1]
signed int的取值范围 [ -2 ^ 31,2 ^ 31 - 1]
unsigned int的取值范围 [ 0,2 ^ 32 - 1]

总结规律:
整数的取值范围无符号:[0,2^n-1] 有符号:[-2^(n-1), 2^(n-1)-1]

char类型在内存中的分配其实可以用下面这副图来表示最好不过了,在实际刷题当中非常有效且实用,强烈建议大家记住这中方法:

我叫它为圆盘法,当它超过该数据类型的最大or最小范围时,它将重新进行一轮新的循环。例如:char的最大整数为127,如果再加+1,它将变为-128;如果是unsigned char型,那么255 + 1就等于0了。
读者可以自行去检测,所有的整型数据类型都可以通用这个方法。

接下来我们来看例子:

其实呢既然是数据已经超出最大范围了,那么就会发生截断,而截断之后又刚好符合我们的圆盘法,所以碰到这种题根本不用去一步步算,我们只需要记住几个临界点就行了,也就是端点。

关于这点博主经常看到的一个例子是出现在二分法求中间点的问题:假设0 <= left <= int的最大正整数, 0 <= right <= int的最大正整数,那么我们求中间点是不是经常这么求 int mid = (left + right) / 2;可这种方法求会出现什么问题呢?如果你的left和right都很大,那么此时就会超过int的最大正整数,进而导致数据溢出,此时由我们的圆盘法可知那么mid此时为负数,所以这样求的答案就必定会有问题,平时我是这么来写的int mid = (right - left) / 2 + left,这样是不是解决了两数相加会溢出的问题呢😜😜

1.6.1 例题

1. 大家先思考思考会得出什么答案?

有没有人觉得是一个随机值呢,-1,-2,-3.......-1000😜😜,这里并没有发现0啊,所以strlen会继续寻找下去直到遇到\0为止。
我们来分析一下,这里当a[i]=-128时,再进行下次循环-128-1是不是退到了127,而strlen遇到\0才停止,所以当减到0时,这个程序才结束,-1 ~ -128 + 127 ~ 0得出的结果为255

2. 这里又会打印多少个hello world呢?

这里依旧是个死循环,我们注意到unsigned char它的取值范围为0~255,那么在这个循环里面i是不是会永远存在呢,再结合之前讲的数据溢出当i = 256时,此时重新进入一个新的轮回 i = 0,所以条件一直会成立下去。

3. 大家好好分析一下这道题,虽然看上去数据非常大,不过只要掌握了圆盘法分分钟拿捏它🙈🙈

~i = 2147483647
-i = 2147483648 == 2147483647 + 1 ==> -2147483648
1 - i = -2147483648 + 1 = -2147483647
-1 - i = -2147483648 - 1 = 2147483647
这样是不是很好理解呢?我们心中要做到有图,这样做题的速度就很快了。

好了,关于整型数据类型的全部知识点就分享到这儿了,下一篇博主将带大家谈谈浮点型在内存中的存储,以及在使用浮点型时经常出现的错误。
如果本篇文章有错误的地方或者不清楚的地方,可以在评论区随时留言或者私信给我哦🙈🙈