进阶——数据的存储

发布于:2022-12-28 ⋅ 阅读:(718) ⋅ 点赞:(0)


目录

1 数据类型的详细介绍

1.1 基本内置类型

1.2 类型基本归类

2 整型在内存中的存储:原码、反码、补码

2.1 原码,反码,补码

2.1.1 原码

2.1.2 反码

2.1.3补码

2.1.4 原理补充

        计算机中的整数有三种2进制表示方法,即原码、反码和补码。

2.1.5 原码与补码的转换方式

3 大小端字节序介绍及判断

3.1 引入

3.2 大小端介绍

3.3 使用代码来判断断当前机器的字节序

3.3.1 思路

3.3.2 方法一

   3.3.3 方法二

   3.3.4 方法三

3.3.5 方法四 

3.4 权重

3.5 例题

3.5.1 有符号位和无符号位的数值的整型提升知识补充

3.5.2 类型的取值范围详细图解——知识补充

3.5.3 例一

3.5.4 例二

3.5.5 例三

3.5.6 例四——运算

3.5.7 例五——无符号变量造成的死循环

3.5.8 例六

3.5.9 例七

        注:条件循环时,注意unsigned类型可能会造成代码的死循环,慎用。

4 浮点型在内存中的存储解析

4.1 常见浮点数类型

4.2 浮点型存储规则

4.2.1 举个栗子

4.2.2 浮点数的存储规则

   4.2.3 使用存储规则解释4.2.1的栗子     


1 数据类型的详细介绍

1.1 基本内置类型

char         // 字符数据类型
short       // 短整型
int         // 整形
long         // 长整型
long long   // 更长的整形
float       // 单精度浮点数
double       // 双精度浮点数
存储空间不同,其存储范围不同

1.2 类型基本归类

1.2.1 整形

数值 有正数负数,有些数值,只有正数,没有负数(身高)                无符号位

                                                有正数,有负数(温度)                       有符号位

     

char  只定义为char,编译器的不同,可能会是有符号的char,也可能是无符号的char,char严格上可以分为三类
                unsigned char
                signed char
short(等价于signed short)
                unsigned short [ int ]
                signed short [ int ]
int(等价于signed int)
                unsigned int
                signed int
long(等价于signed long)
                unsigned long [ int ]
                signed long [ int ]

1.2.2 浮点数

float

double

1.2.3 构造类型(自定义类型)

数组类型

结构体类型 struct

枚举类型 enum

联合类型 union

1.2.4 指针类型

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

1.2.5 空类型

void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型
返回类型
#include<stdio.h>

void test()
{

}

int main()
{
	return 0;
}

函数参数
#include<stdio.h>

void test2(void)//明确不接收参数 
{

}

int main()
{
	return 0;
}

指针

#include<stdio.h>


int main()
{
	void* p = NULL;
	int a = 10;

	void* p1 = &a;

	return 0;
}

        void指针可以存储任何数据类型指针,其可以不用强制类型转换,可临时存放地址,用时可以强制类型转换
       

        但是引用时可能会出错,例如++,--,*等操作,其无确定的类型,所以其具体访问及移动的字节数并不确定

2 整型在内存中的存储:原码、反码、补码

2.1 原码,反码,补码

2.1.1 原码

直接将数值按照正负数的形式翻译成二进制就可以得到原码。

2.1.2 反码

将原码的符号位不变,其他位依次按位取反就可以得到反码。

2.1.3补码

反码 +1 就得到补码。

2.1.4 原理补充

        计算机中的整数有三种2进制表示方法,即原码、反码和补码。

三种表示方法均有 符号位 数值位 两部分,符号位都是用 0 表示 ,用 1 表示
         而数值位正数的原、反、补码都相同,负整数的三种表示方法各不相同
int main()
{
	int a = 20;
	//a四个字节,4byte=32bit
	//00000000 00000000 00000000 00010100
	//00000000 00000000 00000000 00010100
	//00000000 00000000 00000000 00010100

	int b = -10;
	//10000000 00000000 00000000 00001010	原码
	//11111111 11111111 11111111 11110101	反码
	//11111111 11111111 11111111 11110110	补码

}

        我们F10调试并通过窗口监测&b,列调整为4,可以通过内存窗口看见十六进制的存储内容:

             b的补码11111111 11111111 11111111 11110110

对应的十六进制    f     f        f      f        f     f       f      6

对照窗口的 f6  ff   ff   ff,我们可以知道负数的内存存储是以补码的形式存储

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

	//00000000 00000000 00000000 00000001
	//11111111 11111111 11111111 11111111  (-1的补码)
    //100000000 00000000 00000000 00000000(进位丢失)
	//00000000 00000000 00000000 00000000 ==0;

2.1.5 原码与补码的转换方式

 原码到补码——1种方式——原码符号位不变,数值位取反,再加1

	//10000000 00000000 00000000 00000001 (-1的原码)
	//11111111 11111111 11111111 11111110
	//11111111 11111111 11111111 11111111  (-1的补码)

补码到原码——2种方式——先减1,数值位取反,得到原码(原路返回,容易理解)

    //11111111 11111111 11111111 11111111  (-1的补码)
	//11111111 11111111 11111111 11111110 (减1)
    //00000000 00000000 00000000 00000001 (取反)

                                      ——补码取反,再加1,得到原码(与原码到补码的方式相同


	//11111111 11111111 11111111 11111111  (-1的补码)
    //10000000 00000000 00000000 00000000  (取反)
	//00000000 00000000 00000000 00000001 (+1)==1的原码

3 大小端字节序介绍及判断

3.1 引入

     

            b的补码11111111 11111111 11111111 11110110

   对应的十六进制    f     f        f      f        f     f       f      6         

0x十六进制(0 1 2 3 4 5 6 7 8 9 a b c d e f )

        对照窗口的 f6  ff   ff   ff,我们已经了解存储的是补码,但是我们仔细观察,其存储的顺序不太一样,其以字节为单位倒着存储,我们不难意识到当1个数值超过1个字节,要存储到内存中,就有顺序问题:

        例如0x11223344,我们以字节为单位存储,其实可以有多种方式,例如11 22 33 44;

22 11 33 44;33 11 22 44等;

        但是为了方便使用,最终留存了11 22 33 44和44 33 22 11两种方式;

        由此我们引出大小端的概念。

3.2 大小端介绍

大端(存储)模式 ,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式 ,是指数据的低位保存在内存的低地址中,而数据的高位 ,保存在内存的高地址中。
例如0x11 22 33 44        0x十六进制(0 1 2 3 4 5 6 7 8 9 a b c d e f )
            高           低

 小端字节序存储

大端字节序存储

         由此可知,当前VS编译器中数据采用小端字节序存储。

        我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARMDSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式 还是小端模式。
        例如:一个16bit short x ,在内存中的地址为 0x0010 x 的值为 0x1122 ,那么 0x11 为高字节, 0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。

        内存中存放的是补码;

        整型表达式计算使用的内存中的补码计算的;

        打印的和我们看到的是原码

3.3 使用代码来判断断当前机器的字节序

3.3.1 思路

例如0x11 22 33 44        0x十六进制(0 1 2 3 4 5 6 7 8 9 a b c d e f )
            高           低

 小端字节序存储

大端字节序存储

        我们只需判断第一个字节内存储的是11或是44,即可判断是大端字节序或是小端字节序,我们继续简化,我们可以假设a=1,我们只需看二进制1的位置。

        int a=1;(0x00000001)

       

3.3.2 方法一

         我们只需访问一个字节,我们可以用char*指针来操作,但是a是int类型,我们对a强制类型转换,再赋值给char*p,所以我们推出:char* p=(char*)&a;再对*p解引用。

//1表示小端
//0表示大端
int main()
{
	int a = 1;
	char* p = (char*)&a;
	if (*p == 1)
		printf("小端\n");
	else printf("大端\n");

	return 0;
}

   3.3.3 方法二

         我们也可以封装一个函数来实现操作:

//返回1表示小端
//返回0表示大端
int check_sys()
{
	int a = 1;
	char* p = (char*)&a;
	if (*p == 1)
		return 1;
	else return 0;
}



int main()
{
	if(check_sys()==1)
		printf("小端\n");
	else printf("大端\n");
	
	return 0;
}

   3.3.4 方法三

        简化初阶:

//返回1表示小端
//返回0表示大端
int check_sys()
{
	int a = 1;
	//char* p = (char*)&a;
	//if (*p == 1)
    if(*(char*)&a==1)
		return 1;
	else return 0;
}



int main()
{
	if(check_sys()==1)
		printf("小端\n");
	else printf("大端\n");
	
	return 0;
}

3.3.5 方法四 

        简化进阶:

//返回1表示小端
//返回0表示大端
int check_sys()
{
	int a = 1;

	return *(char*)&a;
}



int main()
{
	if(check_sys()==1)
		printf("小端\n");
	else printf("大端\n");
	
	return 0;
}

3.4 权重

10进制:

1                2                3

10^2          10^1           10^0

1*10^2   +  2*10^1   +  3*10^0=123;

十六进制(0x):

1                2                3

16^2          16^1           10^0

1*16^2   +  2*16^1   +  3*16^0=291;

3.5 例题

3.5.1 有符号位和无符号位的数值的整型提升知识补充


    //有符号位的按符号位整型提升
    //无符号位的高位补0

3.5.2 类型的取值范围详细图解——知识补充

整型类型的取值范围限定在:limits.h

3.5.3 例一

//输出什么?
#include <stdio.h>
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; 
}

        我们对其进行详细解释:

int main()
{
	char a = -1;//-1截断后存储到a中
	//10000000 00000000 00000000 00000001(-1的原码)
	//11111111 11111111 11111111 11111110
	//11111111 11111111 11111111 11111111(-1的补码)
	//char两个byte,存放低八个bit位
	//11111111-a

	signed char b = -1;
	//11111111 11111111 11111111 11111111(-1的补码)
	//11111111-b

	//a,b会将最高位作符号位进行处理

	unsigned char c = -1;//无符号char
	//11111111 11111111 11111111 11111111(-1的补码)
	//11111111-c(八位均作为数值位处理)
	//
	printf("a=%d,b=%d,c=%d", a, b, c);


    //有符号位的按符号位整型提升
    //无符号位的高位补0

	
    //a,b打印%d的时候要整型提升,打印原码
	//高位全部补1
	//11111111 11111111 11111111 11111111(-1的补码)
	//11111111 11111111 11111111 11111110
	//10000000 00000000 00000000 00000001(-1的原码)=-1;

	//c打印%d的时候要整型提升,无符号位的高位补0
	//00000000 00000000 00000000 11111111(整型提升)
	//当成有符号位来处理,此处高位是0,正数
	//正数原码、反码、补码均相同
	//00000000 00000000 00000000 11111111(打印的原码)=255;
	return 0;
}

3.5.4 例二

#include <stdio.h>
int main()
{
    char a = -128;

    printf("%u\n",a);
    return 0; 
}

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

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

int main()
{
	char a = -128;
	//
	//10000000 00000000 00000000 10000000
	//11111111 11111111 11111111 01111111
	//11111111 11111111 11111111 10000000
	//截断
	//10000000 -a
	//整形提升
	//char a有符号位,高位置数同符号位
	//11111111 11111111 11111111 10000000
	//%u形式打印,认为内存中存放的数是无符号数
	//原码、反码、补码均相同
	

	printf("%u\n", a);
	return 0;
}

3.5.5 例三

#include <stdio.h>
int main()
{
    char a = 128;

    printf("%u\n",a);
    return 0; 
}
int main()
{
	char a = 128;
	//
	//00000000 00000000 00000000 10000000
	
	
	//截断
	//10000000 -a
	//整形提升
	//char a有符号位,高位置数同符号位
	//11111111 11111111 11111111 10000000
	//%u形式打印,认为内存中存放的数是无符号数
	//原码、反码、补码均相同
	

	printf("%u\n", a);
	return 0;
}

3.5.6 例四——运算

int i= -20;
unsigned  int  j = 10;
printf("%d\n", i+j); 
//按照补码的形式进行运算,最后格式化成为有符号整数
int main()
{
	int i = -20;
	//10000000 00000000 00000000 00010100
	//11111111 11111111 11111111 11101010
	//11111111 11111111 11111111 11101100
	//

	unsigned  int  j = 10;
	//00000000 00000000 00000000 00001010
	//
	
	printf("%d\n", i + j);
	//按照补码的形式进行运算,最后格式化成为有符号整数

	//11111111 11111111 11111111 11101100
	//00000000 00000000 00000000 00001010
	//相加
	//11111111 11111111 11111111 11110110
	//打印原码
	//11111111 11111111 11111111 11110101
	//10000000 00000000 00000000 00001010=-10

	return 0;
}

3.5.7 例五——无符号变量造成的死循环

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

        无符号i,其永远不会小于0,死循环,。

        为了清楚地看清过程,我们使用sleep函数,引用windows.h头文件

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

3.5.8 例六

int main()
{
    char a[1000];
    int i;
    for(i=0; i<1000; i++)
   {
        a[i] = -1-i;
   }
    printf("%d",strlen(a));
    return 0;
 }
int main()
{
	char a[1000];
	//-1 -2 -3 -4…………  -1000
	//但是其值放入char类型变量
	//char的范围(0 1 2 3 ………… 127 -128 -127 ………… -2 -1)(顺序,加法)
	//			(-1 -2 ………… -127 -128 127 ………… 3 2 1 0 -1 -2…………)(逆序,减法)
	//
	int i;
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	printf("%d", strlen(a));
	//strlen 找到ASCII码为0即可;
	//( - 1 - 2 ………… - 127 - 128 127 ………… 3 2 1 0)(逆序,减法)
	//0之前出现255个非\0的数字
	return 0;
}

3.5.9 例七

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

        unsigned char范围0~255,条件恒成立,代码死循环。

        注:条件循环时,注意unsigned类型可能会造成代码的死循环,慎用。

4 浮点型在内存中的存储解析

4.1 常见浮点数类型

3.14159
1E10
浮点数家族包括: float、double、long double 类型。
浮点数表示的范围:float.h中定义

4.2 浮点型存储规则

4.2.1 举个栗子

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

        下面我们来详细分析:

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

        由此推测,浮点数的存与取均与整数对应的存与取有所不同。

4.2.2 浮点数的存储规则

        根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
(-1)^S * M * 2^E
(-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
M表示有效数字,大于等于1,小于2。
2^E表示指数位。

         例如3.3,3.14等,内存对此类数字无法精确保存,浮点数在某种程度上是会丢失一定精度的。

        所以我们只需存储S、M、E三部分存储,需要的时候再还原即可。

IEEE 754规定:
        对于32 位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M

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

IEEE 754对有效数字M和指数E,还有一些特别规定。
        前面说过, 1≤M<2 ,也就是说, M 可以写成 1.xxxxxx 的形式,其中 xxxxxx 表示小数部分。 IEEE 754规定,在计算机内部保存 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 是可以出现负数的,所以IEEE 754 规定,存入内存时 E 的真实值必须再加上一个中间数,对于 8 位的 E ,这个中间数 是 127 ;对于 11 位的 E ,这个中间 数是 1023 。比如, 2^10 E 10 ,所以保存成 32 位浮点数时,必须保存成 10+127=137 ,即10001001。
        举个例子:
int main()
{
	float f = 5.5f;
	//(−1)^0x1.011x2^2
	//S=0
	//M=1.011保留1后面的小数
	//E=2加上中间值127==129
	//0 10000001 011补齐
	//0 10000001 01100000000000000000000
	//0100 0000 1011 0000 0000 0000 0000 0000
	//  4	0	 b	  0	   0    0    0    0(十六进制)
    //并且考虑到VS小端存储

	return 0;
}

        
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 的很小的数字。
0 01111110 00000000000000000000000
E全为1
这时,如果有效数字 M 全为 0 ,表示 ± 无穷大(正负取决于符号位 s );

   4.2.3 使用存储规则解释4.2.1的栗子     

解释前面的题目:
        为什么 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
int n = 9;
	//00000000 00000000 00000000 00001001(9的补码)
	//
	float* pFloat = (float*)&n;

	printf("n的值为:%d\n", n);//(存整数,取整数)
	printf("*pFloat的值为:%f\n", *pFloat);//(存整数,取浮点数)
	//0 00000000000 00000000000000001001
	//E全为0
	//(-1)^0*0.00000000000000001001*2^(1-127=-126)
	//数值太小
	//打印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

	*pFloat = 9.0;
	//9.0
	//1001.0
	//1.001*2^3
	//(-1)^0*1.001*2^3	3+127
	//0 10000010 00100000000000000000000

	printf("num的值为:%d\n", n);//(存浮点数,取整数)
	//0 10000010 00100000000000000000000我们认为其为该数的补码
	//符号位为0,为正数,其原反补相同
	//01000001000100000000000000000000


	printf("*pFloat的值为:%f\n", *pFloat);//(存浮点数,取浮点数)
	//浮点数形式放入,再以浮点数形式拿出;
	//不难理解其如何存再反过来将其还原
	//其取出依旧为9.0


int main()
{
	int n = 9;
	//00000000 00000000 00000000 00001001(9的补码)
	//
	float* pFloat = (float*)&n;

	printf("n的值为:%d\n", n);//(存整数,取整数)
	printf("*pFloat的值为:%f\n", *pFloat);//(存整数,取浮点数)
	//0 00000000000 00000000000000001001
	//E全为0
	//(-1)^0*0.00000000000000001001*2^(1-127=-126)
	//数值太小
	//打印0.000000


	*pFloat = 9.0;
	//9.0
	//1001.0
	//1.001*2^3
	//(-1)^0*1.001*2^3	3+127
	//0 10000010 00100000000000000000000

	printf("num的值为:%d\n", n);//(存浮点数,取整数)
	//0 10000010 00100000000000000000000我们认为其为该数的补码
	//符号位为0,为正数,其原反补相同
	//01000001000100000000000000000000


	printf("*pFloat的值为:%f\n", *pFloat);//(存浮点数,取浮点数)
	//浮点数形式放入,再以浮点数形式拿出;
	//不难理解其如何存再反过来将其还原
	//其取出依旧为9.0

	return 0;
}