学习C语言之 深入了解数据存储

深入了解数据在内存中的存储

1、数据类型介绍

        已了解的基本内置类型:

        char //字符数据类型

        short //短整型

        int //整形

        long //长整型

        long long //更长的整形

        float //单精度浮点数

        double //双精度浮点数

        类型的意义:

        1)使用这个类型开辟内存空间的大小(大小决定了适用范围)。

        2)如何看待内存空间的视角(存储的数据代表什么)。

演示如下:

int main()
{
	int a = 49;		
	char b = '1';
	//调试>窗口>内存>内存1,输入&a,设置列显示4,内存会以4个字节为一行显示
	//可以看到a在内存中储存如下
	//0x00CFFEC8  31 00 00 00
	//即内存中int类型数据代表十进制数49
	
	//输入&b,可以看到b在内存中储存如下
	//0x00CFFEBF  31 cc cc cc
	//即内存中char类型数据的31代表字符‘1’
	
	//穿插讲解:
	//以上内存 0x00CFFEC8 和0x00DFFEBF 各自代表四个字节的起始地址
	//十六进制数 C8 - BF = 8 ,即两个数字相差8个字节。
	//在VS2019 中,两行定义变量中间会差8个字节。
	//在前面调试技巧中,Debug版本和Release版本中提到数组循环进入死循环
	//Debug版本进入死循环就是因为两个变量间差8个字节,而数组越界2个int类型
	//2个int类型正好是8字节,所以其中的i被置0,进入死循环。
	//而Release版本将这个隐患优化消除了,所以没有死循环。

	return 0;
}

穿插讲解部分,为调试技巧中例子,具体代码如下:

int main()
{
	int i = 0;
	int arr[10] = { 0 };
 
	for (i = 0; i <= 12; i++)
	{
		arr[i] = 0;
		printf("haha\n");
	}
	//Debug 模式运行,可以运行,但死循环。
	//Release 模式运行,可以运行,不死循环。
	return 0;
}
 
//进入调试发现,arr[12] 地址和i 地址相同
//arr[12] = 0 即 i = 0,循环重新开始,致死循环

        1.1、类型的基本归类

        整形家族:

        char(内存中存储的是ASCII码,ASCII码是整型)

        unsigned char

        signed char

        short

        unsigned short [ int ]

        signed short [ int ]

        int

        unsigned int

        signed int

        long

        unsigned long [ int ]

        signed long [ int ]

       注: unsigned 指无符号位,即只有正数,没有负数。

        浮点数家族:

        float

        double

        long double

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

        数组类型

        结构体类型 struct

        枚举类型 enum

        联合类型 union

        指针类型:

        int *pi

        char *pc

        float *pf

        void *pv

        空类型(无类型):

        函数返回类型:void test()

        函数参数:test(void)

        指针类型:void* P

2、整形在内存中的存储

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

        整型数表示的范围在 limits.h 中定义。

        2.1、原码、反码、补码

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

        三种表示方法均有符号位和数值位两部分,符号位都是用 0 表示“正”,用 1 表示“负”,正整数的原、反、补码都相同,而负整数的数值位三种表示方法各不相同。

        1)原码:符号位为1,其余位按照正整数的形式翻译成二进制。

        2)反码:符号位为1,其余位按位取反。

        3)补码:符号位为1,其余位在反码基础上+1。

        对于整型来说:数据存放内存中其实存放的是补码。

        早期计算机计算负数值、减法等比较麻烦,后来人们发现,通过补码的形式可以将符号位和数值域统一,可以方便硬件设计。

演示如下:

int main()
{

	int a = 10;	
	int b = -10;
	//a 是十进制数10,转换成2进制
	//00000000 00000000 00000000 00001010-原码、反码、补码

	//b 是十进制数-10,转换成2进制
	//10000000 00000000 00000000 00001010-原码
	//11111111 11111111 11111111 11110101-反码
	//11111111 11111111 11111111 11110110-补码
	
	//好处1、可以表示负数
	//计算机本身只能表示0 1 这两个正数,没法表示 负号 -
	//通过补码的形式可以表示负数。
	
	//好处2、方便计算减法。
	// 计算机本身是没有减法运算的,但通过补码可以将减法转换成加法。
	// 如10-10 = 10+(-10)
	//00000000 00000000 00000000 00001010-补码,a = 10
	//11111111 11111111 11111111 11110110-补码,b = -10
	//相加时从地位往高位计算,即从右往左计算,可得
	//00000000 00000000 00000000 00000000-补码,结果为0
	//用其他数值测试,同样正确。

	//好处3、方便硬件电路。
	//原码转换成补码存储,提取的时候可以用同样的过程提取
	//11111111 11111111 11111111 11110110-补码,b = -10
	//10000000 00000000 00000000 00001001-补码的反码
	//10000000 00000000 00000000 00001010-补码的反码+1,b=-10
	//用其他数测试,同样正确。
	//可见从原码到补码的计算过程,跟从补码到原码的计算过程一致。
	//计算机计算时,通过硬件电路,则两者的硬件电路可以一致。


	return 0;
}

其他数值测试步骤相同。 

       2.2、大小端介绍

        计算机存储数据时分为大小端存储模式。

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

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

        大小端存储模式的成因:

        在计算机系统中,是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char类型外,还有16 bit的short类型,32 bit的int类型(要看具体的编译器)。另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在如何排列多个字节的问题。

        因此,出现了大端存储模式和小端存储模式。

        例如:

        一个16 bit的short型数据X,在内存中的地址为 0x0010,X的值为0x1122,那么 0x11为高字节,0x22为低字节。

        对于大端存储模式,就会将高字节的0x11放在低地址中,即地址0x0010中,将低字节的0x22放在高地址中,即地址0x0011中。

        对于小端存储模式,就会将高字节的0x11放在高地址中,即地址0x0011中,将低字节的0x22放在低地址中,即地址0x0010中。

        常用的X86结构式小端存储模式,而keil C51则为大端存储模式。很多ARM,DSP都是小端存储模式。有些ARM处理器可以由硬件选择是大端模式还是小端模式。

演示如下:

//VS2019 C语言Debug环境下
int main()
{
	
	int a = 2;
	int b = -2;

	//数据在内存中以二进制存储,二进制表示
	//00000000 00000000 00000000 00000010——补码 a= 2
	//10000000 00000000 00000000 00000010——原码 b=-2
	//11111111 11111111 11111111 11111101——反码 b=-2
	//11111111 11111111 11111111 11111110——补码 b=-2

	//为方便显示,内存窗口以16进制表示地址,十六进制表示
	//0   0    0   0    0   0    0   2   ——补码 a= 2
	//F   F    F   F    F   F    F   E   ——补码 b=-2
	
	//调试>窗口>内存>内存1,输入&a,设置列显示4
	//可以看到a在内存中储存如下
	//0x0028FEC0  02 00 00 00
	//可以看到b在内存中储存如下
	//0x0028FEB4  fe ff ff ff

	//可以看到,a代表的实际数字是 00 00 00 02
	//而内存中a的存储是按 02 00 00 00顺序
	//即内存中数据以字节单位按从低位到高位排序,这就是小端存储
	//以b= -2 验证
	//b代表的实际数字补码是FF FF FF FE
	//而内存中b的存储按 FE FF FF FF顺序
	//同样以字节为单位,从地位到高位排序

	return 0;
}

验证大小端存储模式:

//判断是大端存储模式还是小端存储模式
int main()
{
	int a = 1;				
	//a在内存中的数据应该是 
	//00000000 00000000 00000000 00000001 
	//二进制看起来比较麻烦,转化为十六进制表示为
	//00 00 00 01
	char* pa = (char*)&a;	//取出整型数值a首地址pa。
	//如果是大端存储,a的首地址上应该是 00
	//如果是小端存储,a的首地址上应该是 01

	if (*pa == 1)			
	{
		printf("小端存储模式\n");	//打印结果 小端存储模式 。
	}
	else
	{
		printf("大端存储模式\n");
	}

	return 0;
}

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

        浮点数包括:float、double、long double类型。

        浮点数表示的范围在 float.h 中定义。

        3.1、浮点数存储规则

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

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

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

        M 表示有效数字,M ≥1,M<2。

        2^E 表示指数位。

        举例来说:

        十进制的5.0,写成二进制是101.0,相当于1.01 × 2^2 。

        按照上面V的格式,可得出 S = 0;M = 1.01;E = 2。

        十进制的 -5.0,写成二进制是 -101.0,相当于 - 1.01 × 2^2。

        按照上面V的格式,可得出 S = 1;M = 1.01;E = 2。

        3.2、IEEE 754规定

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

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

                3.2.3、有效数字M和指数E特别规定

                        1)有效数字M

                        由于M取值范围是 1≤M<2,即1.xxxxxxxx,其中xxxxxxxx表示小数部分。为了节省一位有效数字,规定小数点前面的 1 默认省略,读取的时候自动加1。

                        比如1.01保存的时候,有效数字 M = .01。(.是为了示意,实际不保存)

                        以32为浮点数为例,M只有23位,但是第一位的1默认省略后,可以等效为24有效数字。

                        2)指数E

                        指数E是一个无符号整数,即它的取值是全正的。但由于科学计数法中的指数会出现负数,为方便表示,IEEE754规定,所有数值的指数存入内存时,都加上中间数,对8位的E,加上127(对11位的E,加上1023)。

                        这样即使出现负指数,也能用正数表示。如 2^(-3),保存成32位浮点数时,指数E须保存成 -3+127=124,即0111 1100。同理,2^10,保存时指数E为10+127=137,即1000 1001。

                        3)关于有效数字M和指数E的例外情况1

                        当E全为0时,规定浮点数的指数E为-126,且有效数字M的小数点 . 前不加1,而是加0。这样是为了表示正±0,以及接近于0的很小的数字。

                        例如内存中二进制数 0000 0000 0000 0000 0000 0000 0000 1001,还原时。由于指数E所在的八位(第二位到第九位)全为0,所以还原时,数值V=(-1)^0 × 0.000 0000 0000 0000 0000 1001 × 2^(-126) = 1.001 ×2^(-146)。

                        显然,V是一个接近于0的很小的数字。

                        4)关于有效数字M和指数E的例外情况2

                        当E全为1,且有效数字M全为0时,表示±无穷大(正负取决于符号位S)。

演示如下:

//浮点型数在内存中存储验证
int main()
{
	int a = 5;				//定义整型变量 a = 5
	//a在内存中存储为二进制,二进制表示为
	//00000000 00000000 00000000 00000101

	float* pa = (float*)&a;	//定义浮点型指针*pa指向变量a所在的地址。

	printf("%d\n", a);		
	//以有符号整型数的方式读取变量a的数值,打印结果 5 。

	printf("%f\n", *pa);
	//以浮点型数的方式读取地址pa中的数值,打印结果0.000000 。
	// 
	//分析,a的二进制如下
	//00000000 00000000 00000000 00000101
	//以浮点型数的方式读取,先看符号位,即第一位 S = 0,正数
	//再看指数位,即第二位到第九位,全为0,表示 E= -126。
	//最后看有效数字位 M 。由于E全为0,则M读取时不在前面默认+1.
	//即M = 0.0000000 00000000 00000101 ,是一个非常小的数字
	//浮点数 v = (-1)^S * M * 2^E
	//则*pa = (-1)^0 * 0.0000000 00000000 00000101 * 2^(-126) =1.01*2^(-147)
	//因此,浮点型数*pa 的数值打印是0.000000 。 

	float b = 5.0;		//定义浮点型变量 b = 5.0,二进制表示为 101 =(-1)^0 * 1.01 * 2^2
	//b在内存中存储为二进制,二进制表示为
	//0 10000001 0100000 00000000 00000000
	//其中第一位0代表符号位0,第二位至第九位10000001 代表指数位 2+127
	//第十位往后代表有效数字 (1).0100000 00000000 00000000 其中首位的1默认省略。

	int* pb = (int*)&b;	//定义整型指针*pb指向变量b所在的地址。

	printf("%d\n", *pb);	
	//以整型数的方式读取地址pb中的数值,打印结果是1084227584 。
	//分析,b的二进制如下
	//01000000 10100000 00000000 00000000
	//整型直接读取为 2^(30)+2^(23)+2^(21) = 1084227584 。
	//因此,整型数*pb 的数值打印是 1084227584 。

	printf("%f\n", b);		
	//以浮点型数的方式读取地址pb中的数值,打印结果是5.000000 。

	return 0;
}

  • 11
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值