资讯专栏INFORMATION COLUMN

【C语言进阶学习】一、数据的存储 (深度剖析数据在内存中的存储)

AprilJ / 3729人阅读

摘要:的理解和区别代表有符号,整数在内存中存储的二进制位的最高位为符号位,表示负数,表示正数。那接下来我们来学习数据在所开辟的内存空间时如何存储的。请看下面例子为什么内存中存储的是补码对于整数来说数据存放内存中其实存放的是补码。


一、数据类型介绍

1、内置类型

其实我们已经学过了基本的内置类型,如下:

类型说明长度
char字符型1个字节
short短整型2个字节
int整型4个字节
long长整型4个字节
long long长长整型8个字节
float单精度浮点型4个字节
double双精度浮点型8个字节

2、自定义类型

用户自定义的类型,例如:我们学过的结构体类型。

3、类型的意义

1.使用这个类型开辟内存空间的大小(大小决定了使用范围)
比如说使用char类型创建的变量,开辟的内存空间是1个字节,而使用int类型创建的变量,开辟的内存空间是4个字节。
2.如何看待看待内存空间的视角
比如我们创建以下变量:

int a = 10;
float f = 10.0f;

这里的a和f所占内存大小都是4个字节,但是我们在看待a的时候,因为a的类型是int,所以我们会把a看做整型,而在看待f的时候,因为f的类型是float,所以我们会把b看做浮点数(而非整型)。


二、类型的基本归类

1、整型家族

整型家族包括一下类型:

unsigned (无符号) signed(有符号)

我们在编写代码声明变量时,[int] 可以省略不写,例如:
unsigned short a = 0;或者 unsigend short int a = 0;两种写法等价。

注意:char也算到整型家族里面,因为字符在存储的时候,存储的是字符所对应的ASCII值(整数),例如:字符 ‘a’ 对应的ASCII值为97。

unsigned / signed的理解和区别:
signed代表有符号,整数在内存中存储的二进制位的最高位为符号位,1表示负数,0表示正数。

unsigned代表无符号,均为正数,最高位是有效位,不为符号位。

例如char类型,一个char是一个字节 = 8bit,假如一个整数内存中存放的二进制的补码为11111111, 如果是signed char,则最高位1是符号位,表示 - 1,如果是unsigned char,则最高位是有效位,不为符号位,它的值是一个正数255。

unsigned char与signed char两者的取值范围:

通过同样的方法,我们也可以推算出其他类型的取值范围。

注:char的类型到底是unsigned char还是signed char是不确定的,取决于编译器。 (大部分编译器下,char是 signed char)

其他类型(short、int、long)默认都是有符号的(signed)。
short == signed short
int == signed int
long == signed long
如果要定义成无符号的,则必须自己声明出来,例如:
unsigned short
unsigned int
unsigned long


2、浮点数家族

浮点数家族包含两类,均是小数(浮点数没有signed 和unsigned区分)
float - - - 单精度浮点型(4字节)
double - - - 双精度浮点型(8字节,精度更高)

整型和浮点型属于内置类型:


3、构造类型

构造类型属于自定义类型。

构造类型包含:

注:数组的元素类型和元素个数不一样,则数组类型也会发生变化。(比如:int [10]与int [5]元素个数不一样)所以数组类型也是自定义类型(自己构造的类型)


4、指针类型

指针类型有:

指针类型的使用以及意义,我们在学习初阶指针时,详细讲过:忘记的同学可以点击去查看:
【C语言初阶】初始指针


5、空类型

void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型。
例如:


三、整型在内存中的存储

我们现在可以知道一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
那接下来我们来学习数据在所开辟的内存空间时如何存储的。

1、源码、反码、补码

源码、反码、补码的概念我们在讲位操作符和移位操作符时,已经提到过,现在我们在这在进行详细的讲解。

计算机中的整数有三种表示方法,即原码反码补码
三种表示方法均有符号位数值位两部分,符号位都是用0表“正”,用1表示“负”,而数值位有分两种情况,正整数三种表示方法相同,负整数的三种表示方法各不相同

举例:如图

解析:
对于正整数:正整数的源码、反码、补码相同。

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

请看下面例子:

2、为什么内存中存储的是补码

对于整数来说:数据存放内存中其实存放的是补码。
为什么呢?
我们首先看一下1-1这个例子:

①先按照原码的方式去计算:

②接下来用补码来进行计算:

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


怎么理解补码与原码相互转换,其运算过程是相同的?(以下运算,符号位均不变)
原码->取反 + 1->补码
补码->取反 + 1->原码

根据我们讲过的
原码通过符号位不变其他位按位取反得到反码,然后反码+1得到补码(原码->取反 + 1->补码)
反过来,我们想通过补码得到源码,则只需:
补码减一得到反码,然后反码的符号位不变,其他位按位取反得到源码(补码 -> - 1 取反->原码)

那么,我们还有一种方法可以通过补码得到源码:
首先补码的符号位不变,其他位按位取反得到反码,然后反码+1得到源码(补码->取反 + 1->原码)

例如 - 1:
11111111 11111111 11111111 11111111 - 补码
补码->取反 + 1->原码
10000000 00000000 00000000 00000000 - 取反
10000000 00000000 00000000 00000001 - +1
补码 -> - 1 取反->原码
11111111 11111111 11111111 11111110 - -1
10000000 00000000 00000000 00000001 - 取反
最终得到的结果均是:10000000 00000000 00000000 00000001


四、大小端介绍

1、什么是大端小端

首先请看下面调试情况:

我们a输入的16进制形式是11223344,在内存中存储的时候却是44332211,可以发现是倒着存储的,而且是以字节为单位存储的,地址是从左往右依次递增的。

其实数据在内存中的存储形式可以分两种,如图:

总结:
大端字节序存储:是指把一个数字的低位字节的内容存放在内存的高地址中,把高位字节的内容,存放在内存的低地址中;
小端字节序存储:是指把一个数字的低位字节的内容存放在内存的低地址中,把高位字节的内容,存放在内存的高地址中;


2、为什么有大端小端

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

3、面试题

请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。
解答:
大端字节序存储:是指把一个数字的低位字节的内容存放在内存的高地址中,把高位字节的内容,存放在内存的低地址中;

小端字节序存储:是指把一个数字的低位字节的内容存放在内存的低地址中,把高位字节的内容,存放在内存的高地址中;

如何判断大端、小端(字节序)呢?
思路:
假如我们利用 int a = 1;来判断当前机器的字节序.
a = 1,用16进制显示是 0x 00 00 01,则:

大端字节序存储形式为:00 00 01
大端字节序存储形式为:01 00 00

所以只需将a的第一个字节内容拿出来,判断其是1还是0,1为小端,0为大端。

代码实现:

#include int main(){	int a = 1;	//0x 00 00 01 --- 16进制显示	//小端:01 00 00	//大端:00 00 01	char* p = (char*)&a;	if (*p == 1)	{		printf("小端/n");	}	else	{		printf("大端/n");	}	return 0;}

我们将判断大小端的功能封装成函数形式:

int check_sys(){	int a = 1;	//0x 00 00 01 --- 16进制显示	//小端:01 00 00	//大端:00 00 01	char* p = (char*)&a;	if(*p == 1)		return 1;	else		return 0;}int main(){	//如果是大端,返回0	//如果是小端,返回1	int ret = check_sys();	if (1 == ret)	{		printf("小端/n");	}	else	{		printf("大端/n");	}	return 0;}

我们可以对这个函数进行优化:

int check_sys(){	int a = 1;	char* p = (char*)&a;	return *p;}

再继续优化:

int check_sys(){	int a = 1;	return *(char*)&a;}

五、练习

1~7道练习判断输出为何

1、下面这段代码的结果是什么?

#include int main(){	char a = -1;	signed char b = -1;	unsigned char c = -1;	printf("a=%d,b=%d,c=%d", a, b, c);	return 0;}

运行结果:

代码分析:

2、下面这段代码的结果是什么?

#include int main(){    char a = -128;    printf("%u/n",a);    return 0; }

运行结果:

代码分析:

3、下面这段代码的结果是什么?

#include int main(){    char a = 128;    printf("%u/n",a);    return 0; }

运行结果:

代码分析:


4、下面这段代码的结果是什么?

#include int main(){	int i = -20;	unsigned int j = 10;	printf("%d/n", i + j);	return 0;}

运行结果:

代码分析:

5、下面这段代码的结果是什么?

#include int main(){	unsigned int i;	for (i = 9; i >= 0; i--) 	{		printf("%u/n", i);	}	return 0;}

运行结果:死循环

为了能更好的观察到代码细节的变化,我们加上Sleep(1000),每打印一个数据都暂停1000ms = 1s。

#include #include int main(){	unsigned int i;	for (i = 9; i >= 0; i--) 	{		printf("%u/n", i);		Sleep(1000);	}	return 0;}


代码分析:
为什么会发生死循环呢?

unsigned int i
i为无符号整型,恒>=0,循环语句的判断条件恒成立。

当 i一直减到-1时,存放入unsigned int i 中会变为无符号整型,变为一个非常大的正数。

6、下面这段代码的结果是什么?

#include #include int main(){	char a[1000];	int i;	for (i = 0; i<1000; i++)	{		a[i] = -1 - i;	}	printf("%d", strlen(a));	return 0;}

运行结果:

代码分析:
我们利用strlen函数求字符数组a的字符串长度,意思就是求在 ‘/0’ ,之前字符的个数。而 ‘/0’ 的ASCII码值为0 。所以就是求0之前的字符个数。

我们在上方讲到,char类型的取值范围为 - 128 — 127,如图:

我们可以发现这么一个规律,当我们从上往下依次加1,当加到127时,再加1就变成-128,当我们继续加1,直到-1时,再加1,就会发现,值又变回0,所以这可以看成是一个循环。请看下面这幅图,更方便理解:

我们可以看出,代码是每次减1进行赋值,所以:arr[0] = -1, 从 - 1 —> 0要经过 - 1, - 2, - 3,…… - 128, 127, 126……3, 2, 1, 0…
128 + 127 = 255个数字,将最后一位的/0排除(strlen在计算字符串长度的时候,遇到/0停止且不计算/0的长度),所以最后的结果是255。

7、下面这段代码的结果是什么?

#include unsigned char i = 0;int main(){	for (i = 0; i <= 255; i++)	{		printf("hello world/n");	}	return 0;}

运行结果:和第五题问题的原理相似,死循环打印Hello world

分析:
因为,unsigned char 的取值范围是 0~255,如图:

当我们 i 自增到255时,再加1,就会发生截断,值又变回0了。
所以条件判断部分恒<=255,从而造成死循环。

六、浮点型在内存中的存储

1、浮点型数据基础知识
常见的浮点数:3.14159,1E10(1.0 x 10^10)
浮点数家族包括:float、double、long double类型
浮点数表示的范围 : float.h中定义
整型家族表示的范围:limits.h中定义

2、如何查看float.h和limits.h呢?
查看limits.h文件方法:


这里就是所有整型家族表示的范围。

查看float.h文件方法:

我们可以先找到VS2019安装的最外层文件夹,在该文件下搜索float.h
找到对应文件后,可以将文件拖进VS2019打开查看



3、举例
下面通过一个例子,来了解浮点型再内存中的存储

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

运行结果:

num 和 *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?

浮点数存储规则
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:

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

例如:
十进制的5.5,写成二进制是101.1
为什么5.5,写成二进制是101.1,而不是101.101呢?如图:


因为101.1小数点后面的1数表示2^-1,即1/2。以此类推,越往后,就分别表示:2 ^ -2,2 ^ -3…

所以5.5的二进制利用科学计数法可以写为:1.011*2^2。对应上面的公式则表示为:
(-1) ^ 0 * 1.011 *2^2
可以得出:
S = 0
M = 1.011
E = 2

再比如:
十进制的9.0,写成二进制是1001.0,利用科学计数法可以写为 :1.001*2^3
对应上面的公式则表示为:(-1) ^ 0 * 1.001 *2^3
可以得出:
S = 0
M = 1.001
E = 3


IEEE754规定:

对于32位的浮点数(float),最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。

对于64位的浮点数(double),最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M

了解了这些之后,那M和E具体是如何存储的呢?

IEEE 754对有效数字M和指数E,还有一些特别规定。

前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。

IEEE754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。
比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。
以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字

至于指数E,情况就比较复杂。

首先,E为一个无符号整数(unsigned int)

这意味着,如果E为8位,它的取值范围为0 ~ 255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE754规定,存入内存时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,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制表示形式为
0 01111110 00000000000000000000000

E全为0

这时,浮点数的指数E等于1 - 127(或者1 -1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。(规定的)

E全为1

这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)

关于浮点数的表示规则就这么多。

了解以上规则之后,请看下面例子,观察浮点数的存储:

#include int main(){	float f = 5.5f;//不加f,编译器默认存储double类型	//101.1  --- 二进制表示形式	//(-1)^0 * 1.011 * 2^2	//S=0	//M=1.011	//E=2   +127 存储(129)	//0 10000001 01100000000000000000000	//S    E            M	//0100 0000 1011 0000 0000 0000 0000 0000	//40 B0 00 00 --- 以16进制存储	//我们可以按F10调试查看内存	return 0;}


可以直观看出,在内存中存储的结果与我们运算的结果一致,这里倒着存储,是因为小端字节序存储方式。

解析上方例题:

#include int main(){	int n = 9;	00000000000000000000000000001001	float *pFloat = (float *)&n;	//*pFloat---是以浮点数的视角去访问n的四个字节,就会认为n的4个字节中放的是浮点数,所以n以浮点数存储的形式为:	//0 00000000 00000000000000000001001	//E为全0	//所以E直接就是 1-127 = -126	//M = 0.00000000000000000001001	//结果为:0.00000000000000000001001 * 2^-126 无穷小的数字	printf("n的值为:%d/n", n);//9	printf("*pFloat的值为:%f/n", *pFloat);//0.000000 打印结果小数点及其后六位(精度)	*pFloat = 9.0;	//*pFloat是以浮点数的视角观察n的4个字节的	//所以以浮点数的形式存储9.0	//1001.0 --- 9.0的二进制表示形式	//(-1)^0 * 1.001*2^3	//S=0	//E = 3	//M = 1.001	//存储结果:	//0 10000010 00100000000000000000000	printf("num的值为:%d/n", n);	//以有符号的形式打印整数	//将01000001000100000000000000000000看作一个有符号的整数,所以它的源反补相同,打印结果为1091567616	printf("*pFloat的值为:%f/n", *pFloat);//以浮点型打印,所以值就为9.000000	return 0;}


好了,到此数据的存储就讲完了,希望大佬帮忙点评呀 ?

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/119108.html

相关文章

  • C语言进阶问:数据内存中是如何存储?(手把手带你深度剖析数据内卒中存储,超全解析,码住不

    摘要:在符号位中,表示正,表示负。我们知道对于整型来说,内存中存放的是该数的补码。在计算机系统中,数值一律用补码来表示和存储。表示有效数字,。规定对于位的浮点数,最高的位是 ...

    ghnor 评论0 收藏0
  • C语言】从入门到入土(进阶数据存储

    摘要:还不清楚原码反码补码的可以到语言从入门到入土操作符篇中的移位操作符处学习一下。比如原码反码补码原码显示值补码数据存放内存中其实存放的是补码补码的表示与存储在计算机系统中,数值一律用补码来表示和存储。 ...

    mcterry 评论0 收藏0
  • Java开发

    摘要:大多数待遇丰厚的开发职位都要求开发者精通多线程技术并且有丰富的程序开发调试优化经验,所以线程相关的问题在面试中经常会被提到。将对象编码为字节流称之为序列化,反之将字节流重建成对象称之为反序列化。 JVM 内存溢出实例 - 实战 JVM(二) 介绍 JVM 内存溢出产生情况分析 Java - 注解详解 详细介绍 Java 注解的使用,有利于学习编译时注解 Java 程序员快速上手 Kot...

    LuDongWei 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<