c语言入门---数据在内存中的存储

一.数据类型的介绍

根据我们之前的学习我们知道我们c语言有如下几个数据的类型:

  1. char(字符数据类型)
  2. short(短整型)
  3. int(整型)
  4. long(长整型)
  5. long long(更长的整型)
  6. float(单精度浮点型)
  7. double(双进度浮点型)

这些不同的类型都有着不同的意义,我们这里的不同类型那就有着不同的意义,比如说这里的不同的类型就决定着使用这个类型开辟内存空间的大小是多少,以及如何看待我们的内存空间的视角,我们之前讲过我们int和float类型的变量在向内存申请空间的时候同样是4个字节,里面就算装着的是同样的数据,但是我们在把内存中的数据往外拿的时候他就会以不同的法则向外转换这个数据,以及转换多大的数据,所以我们这里的整型就有整型的法则,浮点型就有浮点型的法则,所以我们c语言这里不同类型就有着不同的意义。那么我们将c语言的类型分为几大家族,那么我们这里首先就来介绍介绍整型家族。

二.类型的分类

1.整型家族

我们首先看看整型家族有什么:

请添加图片描述
看了这个图片有小伙伴们就要说了,啊为啥我们的char算到整型家族里面去啊?他不是字符类型吗?啊那么这里我就要说了虽然我们平时说的那些字符看起来不是数字,但是这些字符在内存中存储的本质还是ascall码值,也就是整型,所以我们这里还是将我们的字符类型划分到整型家族里面,然后有些小伙伴看到下面的signed xxxx表示了困惑说这是什么意思啊?我咋没见过这个东西,那么我们这里就说啊平时我们在创建变量的时候写的int a,这里一般都是默认的signed int,也就说你写的是int但是编译器默认的是signed int,那么这个signed int的意思就是有符号整型的意思,而如果我们想创建一个无符号整型的话我们就得在创建变量的时候在类型名前面主动加上unsigned,这样的话我们创建的就是一个无符号整型变量,而我们又知道如果是有符号位的变量,那么最高位表示的是符号位,0表示的是正数,而1表示的就是负数,该符号位是不参与数据的大小,而如果是无符号整型的话那么最高位是会参与数据的大小的,而不会表示正负,这就是有符号位和没有符号位的区别,那么这里我要提醒大家一下就是我们这里的char在默认状态下是unsigned还是signed这个在标准当中是未定义的这个完全取决于编译器自己,有些编译器表示在默认状态下是有符号,而有些编译器则表示的无符号,而其他的类型在默认状态下都是有符号位,那么这里大家要注意一下。

2.浮点数家族

请添加图片描述

只要是在c语言当中表示小数的,那么这里都可以使用浮点数,我们c语言当中给出了两种不同的浮点数类型,一个是float表示的是单精度浮点型,另外一个就是double表示的双精度浮点型,那么这两个的区别就在于float的精度低存储的数值范围小,而我们double的精度高,则储存的数据范围更大,那么至于我们c语言是如何来存储我们的浮点数我们后面再来详细的介绍。

3.构造类型

请添加图片描述
那么我们这里就是我们的构造类型,简单的说就是这些类型可以由上面的基本类型来进行组合构成,那么我们这里有小伙伴们看到数组类型就感到有那么点奇怪啊,说数组也有类型吗?那么这里答案是有的,我们之前在讲数组的时候提到过,比如说int arr[8]这是一个整型的数组,那么这个数组的类型就是int [ 8 ],这个方括号表示的就是一个数组,而方括号里面的数字就表示的这个数组的元素个数,而前面的int就表示的这个数组中的每个元素的类型是什么,那么下面的三种构造类型我这里就不多说了,因为刚刚写完关于这三个类型介绍的文章,大家有兴趣的话可以去看看。

4.指针类型

请添加图片描述
那么指针类型相比大家应该都无比的熟悉了吧,我们这里前面的类型不仅能够决定+1的时候能够跳过多少个字节的内容,还能决定该指针在进行解引用的时候能够访问多大内存的空间,以及以什么样的方式来访问这个空间,那么这里我们就不过多的进行介绍了我写的有关指针的那片文章里面写的很详细了,大家可以去看一看。

5.空类型

void就表示的是空类型(无类型),该类型就通常用于函数的返回类型,函数的参数,指针的类型。那么我们这里就得提一句我们的void用于指针的时候他是不能进行解引用的,也不能对其进行加减整数的操作,那么我们在遇到void类型的时候我们得先对齐进行强制类型转换,再来进行使用,我们来看看下面的这个代码:

void test(void)
{
	;
}

我们来看看这个代码中的void表示的意思是什么,首先我们来看第一个void就是test前面的void这个void表示的意思就是这个函数它是不会有返回值的,那么第二个void就是括号里面的void那么表示的意思就是这个函数他在调用的时候是不需要传任何参数的,如果你非要传个参数的话那么我们这里他是会报出警告的,大家注意一下。那么这里我们对类型的分类就分完了,我们接下来就来讲讲整型在内存中的存储是如何来存储的。

三.整形在内存中的存储

我们之前说过创建变量的实质就是在内存当中开辟一个空间,而不同的类型就决定着在内存当中开辟的空间的大小有多大,那么既然我们把内存都开辟好了,那么我们如何把数据往里面存呢?那么这里就是我们下面要讲的内容:
首先我们来聊聊计算机中的整数有三种表示方式,即原码反码和补码,虽然我们这里有三种表示方式,但是我们的正数的这三种表示方式的形式都是一样的即原码反码补码都相同,而我们这三种表示方式均有符号位和数值位两部分其中符号位都是用0来表示正用1表示的是负,而数值位上我们的正数和负数就有些许不同,我们正数的数值位上三种表示方式都是一样的,而我们负整数的三种并表示方法都就各有不同,他们之间的转化形式就是这样的:

  • 原码
  • 直接将二进制按照正负数的形式翻译成二进制就可以了。
  • 反码
  • 将原码的符号位不变,其他位依次按位取反就可以得到了。
  • 补码
  • 反码+1就能够得到补码
    那么这是我们的负数在内存中存储的规则,那么看到这里想必各位小伙伴们还是有一点点的懵,那么我们接下来就来跟大家讲讲几个具体的例子来理解理解:
    比如说3的二进制就是11,但是我们一般都是按照整型的类型来存储这个3,所以我们这里有32个字节,也就是说有32个二进制的数字,那么我们3的原码就是这样:请添加图片描述
    但是这是我们平时人看到的认为的二进制表达情况,但是我们计算机在存储3的时候却不是按照原码的形式来存储,它是按照补码的形式来进行存储,所以我们这里是有一个转换的过程,我们的原码得先转换成反码,再把反码转换成补码,那么这里就会有对应的转换法则,第一点就是正数的原码,反码,补码的形式是一样的,如果是负数的话则原码除符号位以外按位取反得到反码,反码再加1得到补码,那么我们这里就拿-3来举一个例子,我们把二进制的最高位看作是符号位,1就代表着负数,0就代表着正数,那么我们-3的二进制原码的形式就是:
    请添加图片描述
    然后我们再将除最高位的其他位按位取反就可以得到反码其形式为:
    请添加图片描述
    最后再加上1就可以得到其补码的形式为:
    请添加图片描述
    那么看到这里想必大家应该是明白这个负数和正数的原码反码补码之间的转换了,那么大家是否有一个问题我们为什么要这么做啊,为什么要整出原码,反码,补码呢?那么这里我就来讲一下在我们计算机系统中,数值一律用补码来表示和储存,原因就在于,使用补码,可以将符号位和数值域统一进行处理,同时这样的好处就在于可以将加法和剑法进行统一的处理,因为我们的cpu只有加法处理器,也就是说对于减法我们的cpu是无法进行运算的,所以当计算机遇到减法的时候就只能将这个减法变成加法,那怎么来变呢?比如说:3-5我们就可以将其变成3+(-5)这就是我们计算机采用的变换方法,那么这里减法的问题解决了,那么我们这里又如何来表示我们这里的负数呢?那么我们知道负数跟我们的正数那是不一样的,我们的正数1+1就是等于2 ,但是我们的1+(-1)那就只能是等于0了,所以为了解决这个负数的特殊性我们的科学家就通过找规律的方式发现了补码这个东西, 发现将负数的原码转换成补码的形式再与正数的原码进行相加就能实现负数的效果,所以我们这里为了统一性就说我们正数的原码反码补码的形式都是一样的,而我们的负数的形式就得进行一下转换,在我们内存当中存放的都是补码,但是我们在打印出来提取出来的时候就会将其转换为原码,而且科学家发现这种转化的形式还有一个优点就是原码和反码的转换方式都是相同的,不需要额外的硬件电路,原码取反加一得到补码,补码取反加1也可以得到原码,所以这里就是我们这里补码存在的原因极其意义。我们可以将下面的代码进行调试来看看其变量在内存当中存储的情况是什么样的:
#include<stdio.h>
int main()
{
	int a = 20;
	int b = -10;
	return 0;
}

这个是a来内存当中存储的内容:
请添加图片描述
14 00 00 00 我们再来看看变量b在内存当中存储的内容是什么样的:
请添加图片描述
f6 ff ff ff,大家看到这个现象不知道发现了什么问题没?就是我们这里的存储的顺序是不是反了啊,应该是00 00 00 14和ff ff ff f6吧为什么我们这里会将其反着存储呢?那么为了解决这个问题我们就来介绍大小端。

四.大小端的介绍

1.什么是大端小端

我们这里的大端指的就是大端存储模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的地址中。
我们这里的小端指的就是小端存储模式:是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。
那么看到这里想必大家可能对我们这里的几个名词不是那么的熟悉,比如说我们的98765对应的二进制位为:请添加图片描述
那么我们就把数据中靠左边的数据称为高位,把靠右边的数据称为数据的低位,而且我们还知道我们的整型一般的大小都会大于一个字节,那么这就会导致他开辟内存的时候就会申请多个字节,而一个字节对应的就是一个地址,所以我们这里在创建变量的时候这个变量就会有多个地址,而我们在用取地址操作符将这个变量的地址取出来的时候,取得是最开始的地址,换句话说就是较小的那个地址,那么如果我们将低位这边的数据放到地址数较低的位置的话,那么这就是小端存储,相反我们将低位的数据放到地址数较高的地方进行存储的话那么我们就称这个位大端存储,那么这里有小伙伴估计不是很懂为什么会有这样的两个存储方式,那么接下来我们就来讲讲为什么?

2.为什么会有大端和小端

因为在计算机系统中,我们是以字节位单位的,每个地址单元都会对应着一个字节,一个字节的大小都是8个比特位,但是在c语言中出来8个比特的char类型之外,还有16个比特的short,32比特的long类型(需要看具体的编译器),另外对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题,因此就导致了大端存储模式和小端存储模式。例如:一个16bit的short型x,在内存中的地址位0x0010,x的值位0x1122,那么0x11为高字节,0x22为低字节,对于大端模式,就将0x11放到低地址中,即0x0010,0x22放到高地址中,即0x0011中。小端存储模式则刚好相反。我们常用的x86结构是小端模式。而KEIL c51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来决定是大端存储还是小端存储。

3.如何来判断自己的机器是大端还是小端

那么我们知道了什么是大端存储什么是小端存储,那么我们如何来判断我们当前使用的机器是大端存储还是小端存储呢?那么这里我们就可以想一下我们指针相关的内容,我们创建一个int类型的指针,那么这个指针指向的内容就是一个int类型的变量,而一个int类型的变量他是占4个字节的,我们将这个int类型的变量的值初始化为1,那么这个1转换成16进制就应该是00 00 00 01那么我们这里的int*类型的指针里面装的就是这个变量的起始地址,而且我们还知道起始地址是较小的那个地址,那么我们如果能将这个起始地址对应的那一个字节的内容取出来并且打印的话,我们是不是就可以知道到底是大端存储还是小端存储了,为什么因为这个01要么出现在高地址处,要么就会出现在低地址处,那么我们如何那得到这一个字节呢?我们说指针的类型不仅仅决定着加一时跳过几个字节,他还决定着他在解引用时会访问几个字节,那么我们这里是不是就可以通过改变这个指针的类型来实现它访问权限的改变啊,那么我们这里将上面的思路进行汇总就是我们下面的代码:

#include<stdio.h>
int main()
{
	int a = 1;
	int* p = &a;
	int c = *((char*)p);
	if (1 == c)
	{
		printf("小端存储\n");
	}
	else
	{
		printf("大端存储\n");
	}
	return 0;
}

那么这里是我们的一种解决方式,当然我们的方法远远还不止一种,大家可以回想一下我们之前学的一个知识点联合,我们说联合他有一个特点就是它的内部成员会公用同一块的内存,那么我们能不能这么想呢?我们将int类型的变量和char类型的变量联合起来,根据我们联合的计算法则这个联合的大小还是4个字节,那么我们知道的一点就是这个联合内部的char类型变量的地址是这个联合的起始地址,那么我们这里将里面int类型的变量初始化为1,再取出里面char类型变量的值,这样的话我们是不是就可以通过观察这个char类型变量的值就可以判断处事大端存储还是小端存储了,那么我们具体的代码如下:

#include<stdio.h>
union un
{
	char a;
	int b;
};
int main()
{
	union un u1 = {0};
	u1.b = 1;
	if (u1.a == 1)
	{
		printf("小端存储\n");
	}
	else
	{
		printf("大端存储\n");
	}
	return 0;
}

那么看到这里想必大家应该知道了如何来判断该编译器究竟是采用什么样的方式来进行存储,那么接下来我们就来聊聊浮点数在内存当中是如何进行存储的。

五.浮点数在内存中的存储

我们首先看一个例子:

#include<stdio.h>
int main()
{
	int n = 9;
	float* pfloat = (float*)&n;
	printf("n的值为:%d\n", n);
	printf("* pfloat的值为:%f\n", *pfloat);
	*pfloat = 9.0;
	printf("num的值为: %d\n", n);
	printf("*pfloat的值为: %f\n", *pfloat);
	return 0;
}

我们来看看这段代码是什么意思,首先我们创建了一个整型的变量n将n的值赋值为9,然后我们再将n的地址取出来并且将其转换为一个浮点数类型的地址,然后将其赋值给一个浮点数类型的指针,再打印出n的值和对这个浮点数类型指针的值进行解引用的值,那么接下来再对这个浮点数类型指针所指向的地址的内容进行修改改成我们这里的9.0,那么再将我们之前的n的值进行打印,再将浮点数类型的指针进行解引用进行打印,那么我们来看看这段代码的运行的结果:请添加图片描述
看到这里不经让人感觉十分的奇怪,为什么我们这里的会打印出两个非常奇怪的数据呢?这个0.000000是怎么来的啊,这个1091567616又是怎么来的啊?那么我们这里首先得知道一件事情就是我们的整数和浮点数的存储规则肯定是不一样的,那么既然存储的时候规则就不一样,那么我们在提取的时候规则肯定也是不一样的,我们这里的第一次打印的两个数,我们是以整数的形式将其进行存储,那么我用整数的方式进行提取的话,那么我们就可以打印出一个正常的结果,也就是我们这里的9,那么如果我们用整数的方式进行存储而用浮点数的方式进行提取的话,那么这里打印的数据肯定就不是我们想要的数据,那么将其反过来也是一样的,所以就会出现我们上面的情况,那么至于为什么这里会打印出0.000000和1091567616我们这里就得先讲完浮点数在内存中的存储规则再来进行解释了。

浮点数在内存中存储的规则

根据国际标准IEEE(电器和电子工程协会)754,任意一个二进制浮点数v都可以表示成下面的这种形式

请添加图片描述

  • (-1)^S表示符号位,当s=0,v为正数;当s=1,v为负数。
  • M表示有效数字,大于等于1,小于等于2。
  • 2^表示指数位

那么看了上面的规则想必还是有很多的小伙伴不是很能理解上面的意思,那么我们就来举一个例子给大家看看,我们就拿5.0来举一个例子:
首先我们知道我们这里说的5.0肯定是一个十进制的数字,但是我们的计算机只能够识别二进制的数字,所以我们这里就得将这个十进制的5.0转换成为二进制的101.0,那么根据我们上面的规则我们的M表示的是有效数字,他的范围是1到2,那么这个有效数字对应的就是我们这里的101,那么显然我我们这里是超出了这个范围,那么我们这里就得将其进行转化变成1.01*2^2,那么这里要是有小伙伴们不是很能理解的话,我们可以将其类比成十进制的科学计数法:190转换成1.9*10^2,那么我们这里是2进制自然而然也可以将其这么转换,那么我们这个十进制转换成二进制的完整形式就是(-1)^0*1.01*2^2,那么我们这里就可以计算出s=0的,M=1.01,E=2,那么这里有小伙伴们就感到疑惑了,你这里举出来的例子咋小数部分上的值都是0啊,那如果小数部分不为0呢?比如说 5.5 , 7.1 , 9.3呢这些数字我们用二进制又该如何来进行表示呢?那么我们这里就可以根据上面的形式进行改进,就拿我们这里的5.5来举个例子,我们知道5.0的二进制表示形式是 101.0,那么5.5我们就可以对小数点后面数进行操作比如说我们这里可以在小数点后面加一个1,那么这个1表示的值就是2的负一次方,所以我们这里的101.1表示的值就变成了5.5,那么如果我们再往后加一个1呢?就是小数点后有两个1,那么这第二个1表示的值就是2的负二次方,所以我们这里的101.11表示的值就是5.75,那么我们这里就可以依次往后推,小数点后3位的1表示的就是2的负三次方,小数点后四位的1表示的值就是2的负四次方,那么小数点后n位的1表示的值就是2的负n次方,那么这样的话我们是不是就可以在这个二进制表示法中的小数点后面不停的加0或者加1来不断的靠近我们想要表示的小数,但是这里有一点我们要知道的就是,虽然我们这里可以不断的加一或者加0,来使我们这里的精度不断的变高,但是有一点我们要知道的就是,依然有很多的小数我们这里是无法来准确表示出来的,我们只能够做到无线的接近,这也是为什么我们在前面的操作符的时候说到,浮点数最好不用关系操作符来进行比较的原因,那么看到了这里想必大家对我们的小数的二进制表示的方法能够很好的理解了,那么我们接着往下看,根据IEE 754规定:对于32位的浮点数,最高位的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M,那么我们这里的图片的形式就是这样请添加图片描述
那么对于64位的浮点数,我们最高的1位是符号位s,接着的11位是指数E,剩下的52位为有效位数字M,那么我们图片的形式就是这样请添加图片描述
那么我们前面说过,1<=M<2的,也就是说,M的值可以写成1.xxxxxxx的形式其中xxxxxx表示的就是小数部分。那么我们的IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxx部分。比如说保存1.01的时候只保存01,等到读取的时候再把第一位的1加上去,这样做的目的就是节省1位有效数字,以32位浮点数为例,留给M只有23位,将第一位的1舍弃以后等于可以保留24位有效数字。这里是关于我们的有效位M的,那么对于我们的指数E,情况就比较复杂了,首先因为我们的E为一个无符号整数,这就一位者如果E为8位,它的取值范围就位0~255,如果E为11位的话,那么它的取值范围就为0~2047。但是我们知道科学计数法是会出现负数的,那么出现负数的话我们这里的无符号整型就显得有那么点无能为力,那么为了解决这个问题我们就必须在存入内存时对这个E的真实值加上一个中间值使得这个E不会成为负数,那么对于8位的E这个中间数就是127,对于11位的E这个中间数就是1023,比如2^10的E是10,所以保存成32位浮点数的时候必须保存成10+127=137,即10001001,然后我们争对E从内存中取出还可以再分成三种情况:
第一种:E不全为0或者不全为1
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将
有效数字M前加上第一位的1 比如:
0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为
1.0*2^(-1),其阶码为-1+127=126,表示为:
0 01111110 00000000000000000000000
第二种:E全为0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字
第三种:E为全1
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);

解释上面的例题

下面,让我们回到一开始的问题:为什么 0x00000009 还原成浮点数,就成了 0.000000 ?
首先,将 0x00000009 拆分,得到第一位符号位s=0,后面8位的指数 E=00000000 ,最后23位的有效数字M=000 0000 0000 0000 0000 1001。9 -> 0000 0000 0000 0000 0000 0000 0000 1001由于指数E全为0,所以符合上一节的第二种情况。因此,浮点数V就写成:
V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146)
显然,V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000
再看例题的第二部分。
请问浮点数9.0,如何用二进制表示?还原成十进制又是多少?首先,浮点数9.0等于二进制的1001.0,即1.001×2^3。9.0 -> 1001.0 ->(-1)^01.0012^3 -> s=0, M=1.001,E=3+127=130
那么,第一位的符号位s=0,有效数字M等于001后面再加20个0,凑满23位,指数E等于3+127=130, 即10000010。所以,写成二进制形式,应该是s+E+M,即0 10000010 001 0000 0000 0000 0000 0000 这个32位的二进制数,还原成十进制,正是 1091567616 。
点击此处获取代码

  • 20
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叶超凡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值