【c】数据的存储

发布于:2023-01-20 ⋅ 阅读:(327) ⋅ 点赞:(0)

哈喽大家好,大家好,欢迎收看本期博客 ! 

我是Mr.tan

今儿个,给大家分享c语言中的数据存储的相关内容,希望对大家学习c语言能有所帮助。


目录

一、数据类型介绍

1.1、语言内置类型的种类:

1.2、数据类型的分类:

二、整型在内存中如何存储

2.1、原码、反码、补码:

2.2、大小端介绍:

    2.2.1、什么是大小端

2.2.2、为什么有大端和小端

2.2.3、编写代码来判断当前机器的字节序

三、整型提升

3.1、什么是整型提升

3.2、整型提升的意义

3.3、如何整型提升

3.4、signed char和unsigned char的取值范围如何定

 3.5、练习题

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

4.1、引入

4.2、浮点数存储规则

4.3、对于4.1的剖析


一、数据类型介绍

1.1、语言内置类型的种类:

数据类型: 描述:
占存储空间的大小:
char 字符型 1字节
short 短整型 2字节
int 整型 4字节
long 长整型 4字节
long long 更长的整型 8字节
float 单精度浮点型 4字节
double 双精度浮点型 8字节

类型的意义

1、使用这个类型开辟内存空间的大小(要选择适当的类型,防止超出使用范围)。

2、如何看待内存空间的视角(当看到int类型,就知道内存中存储的是整数;当看到double类型,就不难知道内存中存储的是小数)。

1.2、数据类型的分类:

(1)、整型   

 注意:

1、在以上表格当中,对于长整型与短整型,它们后边的 int 被省略掉了,在平时书写的时候也可以不加;

2、把 char 归在整型这一类里,是因为字符型的本质就是ASCII码值,每个字符都有对应的ASCII码值,字符在存储的时候存的就是这个ASCII码值,而它又是整数,所以就归结在整型这一类之中了;

3、对于数值,它都是有正数和负数之分的,而在c语言中,为了把某些值描述的更准确,在对不需要表示正负之分的用 unsigned 来描述,例如:身高;而那些要区分正负的要用 signed 来描述(对于整型、短整型和长整型来说,通常情况下省略不写),例如:温度;

4、c语言中并没有规定 char 到底是哪种类型,这取决于编译器的实现;

(2)、浮点型   

数据类型: 描述:
float 单精度浮点型
double 双精度浮点型

(3)、构造类型   

  也称为自定义类型

数据类型: 描述:
数组类型 ——
struct 结构体类型
enum 枚举类型
union 联合类型

注意:

1、对于语句一和语句二来说,虽然元素类型相同,但是数组中的元素个数却不相同;而对于语句二和语句三来说,虽然元素个数相同,但是元素类型却不相同;所以不管是数组的元素个数发生变化还是数组的类型发生变化,构造出的数组都是不相同的。

char arr[5]; 语句一
char arr[7]; 语句二
int arr[7]; 语句三

2、其他三种类型,在此我们知道就行,详细内容以后会具体的更新哦,请耐心等待吧!

(4)、指针类型   

int*  pi

char*  pj

float*  ps

double*  pl

(5)、空类型   

    void 表示空类型(无类型),通常应用于 函数的返回类型 函数的参数 指针类型
//用于函数的返回类型
void text()
{}

//用于函数的参数
void text2(void)
{}

int main()
{
//用于指针
	void* p = NULL;
	int a = 10;
	void* pl = &a;
}

二、整型在内存中如何存储

变量的创建是要在内存中开辟空间的,空间的大小是根据不同的类型而决定的。

2.1、原码、反码、补码:

    这三种表示方法都有符号位数值位两部分,符号位为“0”表示正,符号位为“1”表示负;整数的原码、反码、补码都是一样的;负数的三种编码各不相同,下面将具描述负数的 三种编码:
原码直接将数值按照正负数的形式翻译成二进制就可以得到原码。
反码 :将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码 :反码+1就得到补码。
int main()
{
	int a = 15;
	//00000000 00000000 00000000 00001111      15的原码
	//00000000 00000000 00000000 00001111      15的反码
	//00000000 00000000 00000000 00001111      15的补码
	int b = -7;
	//10000000 00000000 00000000 00000111      -7原码
	//11111111 11111111 11111111 11111000      -7反码
	//11111111 11111111 11111111 11111001      -7补码
	return 0;
}

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

#include <stdio.h>
int main()
{
	int a = 15;
	//00000000 00000000 00000000 00001111      15的原码
	int b = -7;
	int c = a - 7;
	//因为计算机没有减法,只有加法器,所以转换成15+(-7)
	//  00000000 00000000 00000000 00001111      15的原码
	//  11111111 11111111 11111111 11111001      -7补码
	//1 00000000 00000000 00000000 00001000      15+(-7)的补码,因为是整型,在内存中占32bit,所以多余的那一位直接丢掉
	printf("%d\n", c);
	return 0;
}

  原码→补码有一种方法:

         1、原码取反加一得补码;

  补码→原码有两种方法:

        1、补码减一取反得原码;

        2、补码取反加一得原码。

2.2、大小端介绍:

 从图中我们可以看出来它是以字节为单位在内存中存储的,但是它存储的顺序跟我们想象的可能不太一样,出现这种情况的原因是什么呢?

    2.2.1、什么是大小端

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

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

注意:在vs中是以小端存储模式进行存储的。   

2.2.2、为什么有大端和小端

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

2.2.3、编写代码来判断当前机器的字节序

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

//利用函数
#include <stdio.h>
int check_sys()
{
	int a = 1;
	char* p = (char*)&a;
	if (*p == 1)
		return 1;//返回1表示小端
	else
		return 0;//返回0表示大端
}
int main()
{
	if (check_sys() == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

解析代码:从第一个字节开始,并且只访问第一个字节,最好的办法就是用到一个 char* 的指针,因为对 char* 解引用只访问一个字节(不用 int* 是因为它一次访问四个字节),但是&a 取出来的是第一个字节地址的类型是 int*,此时我们需要强制类型转换 (char*)&a,这里虽然类型发生了变化,但是地址没有变,把这个地址赋给char* 的指针p,这样以来对*p就拿到了①或者②字节的位置中的内容,以此来判断是大端还是小端。


三、整型提升

3.1、什么是整型提升

C的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。用代码举个小例子:

int main()
{
	char a = 5;
	char b = 10;
	char c = a + b;
	printf("%d\n", c);
	return 0;
}

上述代码进行计算的时候,a 和 b 的值被提升为普通整型,然后再执行加法运算,加法运算完成之后,结果将被截断,然后再存储于c之中。

3.2、整型提升的意义

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通常CPU是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

3.3、如何整型提升

下述代码是整型提升的原理分析,在此声明一下,char类型是无法存储一个较大的数的。而整型提升主要针对那些自身大小小于整型大小的变量。

整型提升的规则:先观察其类型,然后对于有符号的数整型提升是按照符号位来提升的,符号位为0则在前面补0,符号位为1则在前面补1;而对于无符号的数整型提升,就单纯的在高位补0;

//代码1
int main()
{
	char a = 5;
	//00000000 00000000 00000000 00000101
	//00000101   a 因为它是char类型的,只能存8个比特位,这8为就是截断之后存到a里边的数据
	char b = 10;
	//00000000 00000000 00000000 00001010
	//00001010   b 因为它是char类型的,只能存8个比特位,这8为就是截断之后存到b里边的数据
	char c = a + b;
	//00000101   a提升前的值
	//00001010   b提升前的值
	//00000000 00000000 00000000 00000101   a提升后的值
	//00000000 00000000 00000000 00001010   b提升后的值
	//00000000 00000000 00000000 00001111   a + b要放到c里面去
	//00001111  c(截断之后得到的)
	printf("%d\n", c);
	//%d十进制的方式打印有符号的整数,它要发生整型提升,提升的时候要看c的类型
	//00000000 00000000 0000000 00001111  提升之后的补码,也是原码(整数的原、反、补都是一样的)  
	return 0;
}


//代码2
int main()
{
	char a = -1;
	//11111111 11111111 11111111 11111111   -1的补码
	//11111111   a(整型提升前)
	//11111111 11111111 11111111 11111111   a(整型提升后)
	signed char b = -1;
	//signed 与 char 是一样
	unsigned char c = -1;
	printf("a=%d,b=%d,c=%d", a, b, c);
	//对char整型提升的时候,char a的高位是符号位,前面补1;
	//11111111 11111111 11111111 11111111  因为是以%d的形式打印,打印的是原码,将补码变为原码得到的十进制数为-1
	// 对unsigned char整型提升的时候,它的高位不是符号位,前面补0
	//00000000 00000000 00000000 11111111 因为是以%d的形式打印,打印的是原码,把它当成一个有符号数的原码进行打印,看它高位为0 认为是正数,从而得到的十进制数为255
	return 0;
}

3.4、signed char和unsigned char的取值范围如何定

如图所示,假设我认为它是signed char,最高位是符号位,然后将所有可能列举出来,在这里规定死1000 0000为最小的数-128。所以signed char的范围是 -128 ~ 127

如图所示,假设我认为它是unsigned char, 最高位不是符号位,然后将所有可能列举出来,所以unsigned char的范围是 0 ~ 255

 3.5、练习题

%u是打印无符号整形,认为内存中存放的补码对应的是一个无符号数;

%d 是打印有符号整形,认为内存中存放的补码对应的是一个有符号数。

无符号数的原码、反码的补码都是一样的。

//习题1
#include <stdio.h>
int main()
{
	char a = -128;
	//11111111 11111111 11111111 10000000    -128的补码
	//10000000    截断后的a
	//11111111 11111111 11111111 10000000     整型提升后的a(无符号数的原、反、补码相同)
	//换算为十进制数为4294967168
	printf("%u\n", a);
	return 0;
}

//习题2
#include <stdio.h>
int main()
{
	char a = 128;
	//00000000 00000000 00000000 10000000    128的补码
	//10000000    截断后的a(最高位为符号位)
	//11111111 11111111 11111111 10000000     整型提升后的a
	//换算为十进制数为4294967168
	printf("%u\n", a);
	return 0;
}

//习题3
int main()
{
	int i = -20;
	//10000000 00000000 00000000 00010100  原码
	//11111111 11111111 11111111 11101011  反码
	//11111111 11111111 11111111 11101100  补码
	unsigned int j = 10;
	//00000000 00000000 00000000 00001010
	printf("%d\n", i + j);
	//11111111 11111111 11111111 11101100  i补码
	//00000000 00000000 00000000 00001010  j补码
	
	//11111111 11111111 11111111 11110110  加之后的补码
	//11111111 11111111 11111111 11110101  加之后的反码
	//10000000 00000000 00000000 00001010  加之后的原码,转换为十进制数为-10

	return 0;
}

   习题4解析:因为i是无符号整型数,所以取值范围一定是大于等于0的,而在循环中的判断条件也是大于等于0的,所以它恒成立,这样就造成了死循环 。当我们--到0的时候,再减就变成了-1,而-1的补码为32个1,当32个1被看作成一个无符号的数,那将是一个非常大的正数。(直截图开始的一小部分)。利用 sleep 可以更清晰的观察数据的变化,如果不加sleep,程序运行的将非常快那样就看不到开头的数据了,用它的时候不要忘记引用头文件哦!

//习题4
#include <stdio.h>
#include <windows.h>
int main()
{
	unsigned int i;
	for (i = 9; i >= 0; i--) 
	{
		printf("%u\n", i);
	}
	Sleep(100);
	return  0;
}

    习题5解析: strlen求字符串的长度,它要关注'\0',而'\0'的ASCII值为0,那相当于当我们循环的时候遇到0就停止。前面我们也提到char的取值范围为-128 ~ 127,当我们一直减一减到-128的时候,再往后就是127了(如图所示,只写了有代表性的部分比特位,剩余的都省略掉了,但是不代表没有哦)。因为它是循环1000次,所以到0之后,接着又是-1,以此类推直至循环完事儿。但是strlen不关注0往后的长度,只需要计算0之前的长度即可,在0之前出现了255个非'\0'的数,长度就为255(-1 ~ -18,1 ~ 127,一共255个数)。

//习题5
#include <stdio.h>
int main()
{
	char a[1000];
	int i;
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	printf("%d", strlen(a));
	return 0;
}


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

整形类型的取值范围限定在:limits.h;浮点型类型的取值范围限定在: float. h。

4.1、引入

此处的知识我们由一段代码引入,运行结果如下图:

#include <stdio.h>
int main()
{
	int n = 9;//&n --> int*
	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;
}

 当我们看到运行结果的时候会发现:

1、以整型的形式放进去,再以整型的形式拿出来,结果没发生变化;

2、以整型的形式放进去,再以浮点数的形式拿出来,结果发生了变化;

3、以浮点数的形式放进去,再以整型的形式拿出来,结果发生了变化;

4、以浮点数的形式放进去,再以浮点数的形式拿出来,结果没发生变化。

从而我们知道了浮点数和整数在内存中存储和取出的形式是有区别的,所以才导致了这些问题。

4.2、浮点数存储规则

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

(-1)^S * M * 2^E

(-1)^S表示符号位,当s = 0,V为正数;当s = 1,V为负数。

M表示有效数字,大于等于1,小于2。

2^E表示指数位。

举例来说:十进制的8.75,写成二进制是1000.11,相当于1.00011×2^3。那么,按照上面V的格式,可以得出S=0,M=1.00011,E=3。

IEEE 754规定: 对于32位(float类型)的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M; 对于64位(double类型)的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。IEEE 754对有效数字M和指数E,还有一些特别规定。

  M:前面说过,1≤M<2 ,也就是说,M可以写成 1.00011 (以8.75为例)的形式,其中00011表示小数部分。IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的00011部分,等到读取的时候,再把第一位的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^3的E是3,所以保存成32位浮点数时,必须保存成3+127=130,即10000010。

//单精度浮点数
int main()
{
	float s = 8.75f;
	//(-1)^0 * 1.00011 * 2^3
	//S = 0
	//M = 1.00011
	//E = 3   他要加上127-->  10000010
	//0 10000010 00011000000000000000000
	//0100 0001 0000 1100 0000 0000 0000 0000
	//41 0c 00 00  转化为十六进制
	return 0;
}

//双精度浮点数
int main()
{
	double s = 8.75;
	//(-1)^0 * 1.00011 * 2^3
	//S = 0
	//M = 1.00011
	//E = 3   他要加上1023-->  10000000010
	//0 10000000010 0001100000000000000000000000000000000000000000000000
	//0100 0000 0010 0001 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
	//40 21 80 00 00 00 00 00 
	return 0;
}

 

 按照S、M和N我们将二进制补码求出来之后转化为十六进制(方便我们观察),当运行时打开内存窗口,我们看到的结果与我们算出来的是一样的(小端存储)。

指数E从内存中取出还可以再分三种情况:

E不全为0或不全为1

这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将 有效数字M前加上第一位的1。

E为全0

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

E为全1

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

4.3、对于4.1的剖析

#include <stdio.h>
int main()
{
	int n = 9;//&n  -->  int*
	//00000000 00000000 0000000 00001001   9的补码
	float* pFloat = (float*)&n;
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	//0 0000000 00000000000000000001001
	//(-1)^0 * 0.00000000000000000001001 * 2^ -16  它会是一个非常非常小的数
	
	*pFloat = 9.0;
	//(-1)^0 * 1.001 * 2^3
	//01000001000100000000000000000000   以浮点形式放进去,再以整型的形式往外拿,那我们就把它当成补码,最高位为0,是个正数,那么这个就是它的原码,它代表的十进制的值是1091567616
	printf("num的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);
	return 0;
}

欢迎大家的收看本期内容,今天的内容到此结束了,对于本期内容到这里就跟大家告别一段落了,让我们期待下一篇的文章到来吧!

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到