数据在内存中的存储(深入理解C语言数据类型)

目录

1.C语言内置类型的介绍

        1.1整形家族

        1.2浮点型家族

        1.3类型的意义

        1.4空类型

 2.整形在内存中的存储

         2.1原码,反码和补码

        2.2大小端字节序

        2.3练习题

                2.3.1下面程序输出的结果是什么?

                2.3.2下面程序输出的结果是什么 

                2.3.3下面程序输出的结果是什么

                2.3.4下面这个程序输出的结果是什么? 

                2.3.5下面的程序输出的结果是什么?

                2.3.6下面程序输出的结果是什么?

                2.3.7下面程序输出的结果什么?

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

        3.1引入       

        3.2浮点数存储规则 


1.C语言内置类型的介绍

        C语言有很多种内置类型大体上可以分为两类,一类是整形家族,另一类是浮点型家族。

        1.1整形家族

        整形家族包括:char- 字符类型(占1个字节的内存空间),short-短整型(占2个字节的内存空间),int-整形(占4个字节的内存空间),long-长整形(占4个字节的内存空间),long long-更长的整形(c99标准引入)(占8个字节的内存空间),它们定义在limit.h里面,为什么这里说long类型跟int类型所占的空间都是四个字节呢,因为c语法规定sizeof(long)>=sizeof(int),所以在32位平台机器上就会有上述的结果,其中要注意的是char类型C语言并未规定它是unsigned char还是 signed char,其他的比如int是signed int是规定 。所以在使用char类型的时候要注意编译器(这时候char类型是怎么样的由编译器决定不同的编译器有不同的结果)。

        1.2浮点型家族

        浮点型家族只有两位成员float-单精度浮点型(占四个字节的内存空间)和double-双精度浮点型(占8个字节的内存空间)。float存储数据精度更低,能表示数据的范围更小,double存储数据精度比float类型高,表示数据的范围更大。他们定义在float.h里面

        1.3类型的意义

        C语言提供了这么多种类型这有什么意义吗?

       意义:使用这个类型开辟空间的大小,以怎么样的视角看待内存空间

        1.4空类型

        在定义函数的时候你是否看到用过void—空类型,这有什么意义吗?请接着向下看:

#include<stdio.h>
void test()
{
	printf("hehe\n");
}
int main()
{
	test(1);
	return 0;
}

 乍一看这个代码好像没什么问题但是你仔细看会发现函数明明没有参数但是我们调用的时候却给它传参了而在如果你编译这个代码是可以编译通过的(我用VS2019测试的)。这样就不是很好,为了避免这种情况我们可以在函数形参那里加void,如下:

 2.整形在内存中的存储

        我们知道创建变量是向内存中开辟空间,不同类型的变量开辟的空间大小是不同的。那接下来我们谈谈数据在所开辟内存中到底是如何存储的?比如

int a = 4;

我们知道是从内存中向a申请4个字节的空间,那a是如何存储的呢?

         2.1原码,反码和补码

        计算机中的整数有三种表示形式:原码反码和补码。三种表示方法均有符号位和数值域两部分组成,符号位都用‘0’表示正数。用‘1’表示负数,而负数的数值域的三种表示方法均不同。

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

反码:将原码的符号位不变数值位按位取反就得到了反码。

补码:  将反码加一就得到了补码。

        比如:

-1的原码为:10000000 00000000 00000000 00000001,

原码取反得到反码:11111111 11111111 11111111 1111110, 

反码加一:11111111 11111111 11111111 11111111。

注意正数的原码,反码和补码是相同的。 

那现在 你是否有疑问整数在内存中是按照那种方式来存储数据的呢?我们不妨打开编译器验证一下:

我们发现-1在内存中确实是以补码的形式存储的,我们打开内存看到的都是8个f,这是因为为了方便表示数据监视里面是以十六进制来表示的,但是在内存中依然是以二进制的方式进行存储的。如果你不相信八个f是十六进制可以打开计算器自己验证一下:

        为什么在内存中是按照补码的形式存储数据的呢,因为这样:符号位就可以和数值域一起运算,同时加法和减法可以一起处理(计算机的CPU只有加法器),此外原码和补码相互转换,其运算过程相同,(补码取反加一也可以得到原码)不需要额外的硬件电路。

        2.2大小端字节序

让我们再看看数据在内存中的存储:

我们发现内存中确实是存的数据的补码到时,怎么存储的顺序是倒着的,这又是为什么呢,如果将数据存储到内存中(按照字节一个一个的存储),假如让你存你会怎么存储?正着存进去?倒着存进取,还是按照其他的方式存进内存中,这就涉及到字节序问题了。

大端字节序:数据的低位放到地址的高位,数据的高位放到地址的低位。

小端字节序:数据的低位放到地址的低位,数据的高位放到地址的高位。

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

比如拿十六进制数:0x11223344来举例(为什么要拿十六进制数来举例,因为我们讨论的是数据按照字节在内存中的存储方式,刚好两个十六进制位是一个字节),图解:

 注意字符类型存储到内存中是没有字节序的,因为字符类型只有一个字节。好现在我们已经知道大小端字节序是什么了,如果让你写一个程序来求当前机器是大端字节序还是小端字节序你怎么求?

这里我提供两种思路,思路1:不同类型的指针解引用能访问不同大小的空间,我们是否能利用指针的这一特性来求解呢?

思路2:利用共用体的特性,将一个不够四字节的值赋给共用体中字节数较大的成员,然后通过共用体中字节序较小的成员来访问数据分析结果是不是就能判断当前机器的字节序了,大家不妨试一试。

代码如下:

int CheckByte1(void)
{
	int a = 1;//定义int类型的a
	char* b =(char*)&a;//取a的地址强转为char*类型的地址
	return *b;//返回指针b所存地址的元素的值
}
int CheckByte2(void)
{
	union byte//定义共用体
	{
		int a;
		char c;
	}u1;
	u1.a = 1;//将1赋值给a
	return u1.c;
}
int main()
{
	int bit = CheckByte1();
	if (1 == bit)//如果返回值是1说明是小端
	{
		printf("当前机器为小端字节序\n");
	}
	else
	{
		printf("当前机器为大端字节序\n");
	}
	return 0;
}

解释:

        CheckByte1:因为将1存入赋值给变量a,变量a为整形占四个字节的内存,如果当前机器是小端字节序则,以char指针访问内存拿到的数据为1的话说明数据1存到这个机器的低地址处。则说明该机器是小端字节序,如果拿到的数据不是1则说明该机器为大端字节序。

        CheckByte2解释:共用体中成员a和c都是从共用体零偏移处存储的变量a占4个字节,变量b占1个字节,所以将1赋值给成员a,再用成员c来访问这块内存如果可以拿到1,则说明是小端字节序,反之则说明是大端字节序。

        2.3练习题

                2.3.1下面程序输出的结果是什么?

        我们已经了解了整形在内存中的存储,大家不妨在做几道练习题来巩固一下学习成果:

//下面程序运行结果是什么

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

        解释程序会死循环因为i是无符号的int所以不会小于零,而循环只有当i小于零的时候才会停止,所以循环会一直进行下去。

                2.3.2下面程序输出的结果是什么 

//程序运行结果是什么

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

        程序会死循环。

        解释:因为程序中定义的  i  是无符号char,而for循环结束的条件是i 小于0,无符号的数是不会小于0的(C语言中),所以程序会死循环。

                2.3.3下面程序输出的结果是什么

//下面这个程序打印的结果是什么?

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

        什么这个程序打印的结果是多少?猜一下,1000?

        答案是255。

        为什么答案是这个呢,因为strlen函数是用来求字符串长度的遇到‘\0’(0也是\0因为它们的ASSCII码值相同)就会结束,而这个数组是char类型的数组,从-1,-2,-3... -127,-128,127,126,125,124...,3,2,1,0。当strlen函数遇到0的时候就终止了,strlen函数计算出来的长度就是0之前的字符个数,一共有255个所以strlen函数求出来的长度为255。

图解:

                2.3.4下面这个程序输出的结果是什么? 


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

         答案是a = -1,b =  -1, c = 255。为什么结果是这个,因为数据在内存中是以补码的形式存储的,a和b是有符号的char(当前平台认为char是有符号的char),将-1赋值给它们,然后以整形的方式打印就是-1,但是c是无符号的char将-1赋值给c相当于将-1的补码(1111 1111 1111 1111  1111 1111 1111 1111)给了变量c,但是c是unsigned char所以变量c的原码反码补码相同,因为字符类型比整形小存到变量c中的时候要发生截断,所以存到变量c中的是1111 1111,这个二进制序列翻译成十进制就是255。

                2.3.5下面的程序输出的结果是什么?


#include <stdio.h>
int main()
{
        char a = -128;
        printf("%u\n",a);
        return 0;
}

         程序输出的结果是4294967168,为什么是这么大的一个数呢,因为-128在内存中是1000 0000这是它的二进制序列,为什么是这个,因为1000 0000其实是负零但是零有一个就可以了,所以就规定1000 0000的值为-128,因为在屏幕上输出的时候,是以整形的形式输出的,但是a是char类型小于整形,所以要进行整形提升。提升之后的二进制序列是 1111 1111 1111 1111 1111 1111 1000 0000,以%u的形式打印,所以打印结果是:4294967168。

                2.3.6下面程序输出的结果是什么?

#include <stdio.h>
int main()
{
        char a = 128;
        printf("%u\n",a);
        return 0;
}

        输出结果:4294967168。 为什么?

        因为变量a是字符类型,但是有符号的char 最大只能存储-128到127的数字,实际上将128赋值给变量a是存不下的,所以变量a存放的是1000 0000(-128),又因为printf函数打印变量a到屏幕是是以%u的形式的输出的,所以变量a在输出的时候要进行整形提升,提升结果是:1111 1111 1111 1111 1111 1111 1000 0000转换为十进制就是4294967168。

                2.3.7下面程序输出的结果什么?

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

        输出结果是-10,为什么是这个结果呢?虽然变量i是 int类型,变量j是unsigned int类型,它们相加要进行 算术转换都转为unsigned int 类型进行相加,可能你看到这里会认为输出的结果会是一个很大的值,但是printf函数输出的时候是以%d的形式输出的(本来如果将-10的补码直接转换为十进制会是一个很大的数字),所以会将-10的补码转换为原码进行输出,所以输出结果依旧是-10

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

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

        现在让我们一起来看看结果吧!

为什么会是这个结果呢,让我们先来了解一下浮点型是在内存中如何存储的再来分析这个问题。

        3.2浮点数存储规则 

       根据国际标准IEEE754,浮点数在内存中是以

 

         其中s表示正负,如果s=0,表示正数,如果s=1,表示负数,M表示有效位数,大于1小于2,2的E次方表示指数位。举个例子:比如5.0,写成二进制位101.0,相当于,1.01*2^2,按照上面的形式可以得出S=0,M=1.01,E=2。写成上面的形式就是-1^0*1.01*2^2(5.0的浮点数表示形式)。

        IEEE 754规定:

        对于32位的浮点数最高的1位是符号位S接下来是8位指数位E,剩下的是23位的有效数字M。如图:

        对于64位的浮点数。最高的1位S是符号位,接下来是11位的指数位E,剩下的是52位的有效数字M,如下图:

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

        前面说过有效数字M是大于1小于2的因为M都在这个区间里面所以在将M存到内存中的时候会将小数点前面的1省略掉。比如保存1.01的时候,就只保存01,读出来的时候再加上即可。这样的目的是为了节省一位有效数字,提高数字的精度。

        至于指数E的情况就比较复杂了,首先E是一个无符号的整数 ,如果E是8位取值范围就是0——255,如果E是11位取值范围就是0——2047。但是我们知道在科学计数法中E是可以出现负数的,所以IEEE 754规定:存入内存中的真实值E必须加上一个中间数,如果是8位的E中间数是127,如果是11位的E中间数是1023。比如2^10,保存为32位的浮点数时。必须保存为10+127=137。即:1000 1001。

        然后E从内存中提取出来还分为三种情况:

        E不为全0或E不为全1:

       这时候浮点数就采取下面的规则:如果是32位的浮点数,E就减去127,得到真实的E,再将有效数字M的前面加上第一位的1。如果是64位的浮点数,E就减去1023,得到真的E, 再将有效数字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为全0则这时候的E等于1-127(或)1023,得到真实值,有效数字M的第一位不在加一而是还原成为:0.xxxxxxxx的小数这样做的目的是为了表示±0,以及接近于0的很小的数字。 

        E为全1:

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

好了浮点数的存储规则就讲到这里了,现在让我们一起来看看前面的这道题目:

将9以整形的形式放到内存中以浮点数的形式从内存中拿出来为什么是0呢,因为9以小端字节序的方式存储到内存中为:0000 0000 0000 0000 0000 0000 0000 1001,而这个二进制序列中指数E为全0符号位S为0,所以最终这个数字就表示:0。

再看将浮点数9.0表示成二进制形式:0 1000 0111    0010 0000 0000 0000 0000  000

9.0 = 1.001 * 2^3; E = 127 + 3 = 130。M = 001。 

这个数字以整形的形式打印出来就是一个很大的数字:1091567616。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值