• 深度剖析数据在内存中的存储


    个人主页:平行线也会相交
    欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 平行线也会相交 原创
    收录于专栏【C/C++
    在这里插入图片描述

    1.数据类型详细介绍

    1.1数据类型介绍

    我们已经知道了基本的内置类型:

    char //字符数据类型
    short //短整型
    int //整型
    long //长整型
    long long //更长的整型
    float //单精度浮点数
    double //双精度浮点数
    //以上类型都是C语言本身具有的类型,叫做内置类型
    //C语言类型分为两类,其一就是上面的内置类型,其二就是自定义类型(也称构造类型)

    类型的意义:1.类型决定了开辟空间的大小(大小又决定了使用范围)。2.看待内存空间的视角不同。
    对于意义2,我们用代码举一个例子:在这里插入图片描述
    在这里插入图片描述
    意义2的意思就是站在整型的角度,内存中存放的当然是整型了;而站在浮点型的角度,内存中存放的当然是浮点型了。

    1.2类型的基本归类

    整型家族

    char
    	unsigned char
    	signed char
    short
    	unsigned short [int]
    	signed short [int]
    int
    	unsigned int
    	signed int
    long
    	unsigned long [int]
    	signed long [int]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    有些小伙伴看到列举的整型家族中有char会感到疑问,char类型怎么能在整型家族中呢?是这样的:由于char类型不好归类,而且char类型在内存中存储的是其ASCII码值,既然存储的是ASCII码值的话,ASCII码值又是一个整数,相当于一个字符在表示的时候是用整数来表示和存储的。因此我们把char归类到整型家族中去。在char类型中又分为有符号(八个比特位中最高位为符号位,如:01 01 00 01)和无符号(八个比特位中最高位不是符号位,如:00 01 00 01),无符号为也就没有正负之分。
    对于无符号数如果把最高位不当成符号位而当成有效位的话,它能表示的数的范围会更大一些。比如:11111111这八个比特位如果最高位这一位不是符号位而是有效位,则把二进制11111111翻译成十进制的话是255;把最高位当成有符号位的话用十进制数来表示是一个负数,即-127
    其实一个char类型的变量如果表示有符号数的话,它的范围是-128127;而如果表示无符号是,它的范围是0255
    浮点型家族

    float//单精度浮点型
    double//双精度浮点型
    
    • 1
    • 2

    构造类型(即自己创造的一些类型):

    数组类型
    结构体类型  struct
    枚举类型    enum
    联合类型    enum
    
    • 1
    • 2
    • 3
    • 4

    指针类型

    int *pi;
    char *pc;
    float *pf;
    void* pv;
    
    • 1
    • 2
    • 3
    • 4

    空类型:

    void表示空类型(无类型)
    通常应用于函数的返回类型、函数的参数、指针类型
    
    • 1
    • 2

    例如:
    在这里插入图片描述

    2.整型在内存中的存储

    我们知道一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同类型数据决定的。

    接下来我们讨论数据在所开辟的内存中到底是如何存储的?
    在这里插入图片描述
    在这里插入图片描述
    在清楚上面图片是什么意思的话,我们需要清楚原码、反码、补码的概念。这里不进行展开说明。

    #include
    int main()
    {
    	int a = 20;
    	//00000000000000000000000000010100原码
    	//00000000000000000000000000010100反码
    	//00000000000000000000000000010100补码
    	int b = -10;
    	//10000000000000000000000000001010原码
    	//11111111111111111111111111110101反码
    	//11111111111111111111111111110110补码
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这里的二进制在内存中展示的时候是以16进制进行展示的(4个二进制位相当于1个16进制位)

    #include
    int main()
    {
    	int a = 20;
    	//00000000000000000000000000010100原码
    	//00000000000000000000000000010100反码
    	//00000000000000000000000000010100补码
    	//0x00000014
    	int b = -10;
    	//10000000000000000000000000001010原码
    	//11111111111111111111111111110101反码
    	//11111111111111111111111111110110补码
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    #include
    int main()
    {
    	int a = 20;
    	//00000000000000000000000000010100原码
    	//00000000000000000000000000010100反码
    	//00000000000000000000000000010100补码
    	//00000014
    	int b = -10;
    	//10000000000000000000000000001010原码
    	//11111111111111111111111111110101反码
    	//11111111111111111111111111110110补码
    	//0xFFFFFFF6
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    直到这里我们知道内存中存一个整数的时候存的是其二进制序列的补码,相信大家依然会有一些疑问:
    在这里插入图片描述
    为什么这里的14 00 00 00f6 ff ff ff是倒着存放的,但是不管怎么讲,整数在内存中存放的是其二进制序列的补码。
    总结:整数存放内存中其实存放的是补码。
    那为什么呢?

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

    刚刚提到CPU中只有加法器,但是当我们使用补码时,加法、减法等就可以进行统一处理了。这里它是怎么进行统一处理的呢?我们举一个例子:
    在这里插入图片描述
    对于乘法和除法的话不就是多加几次的事吗

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

    对于刚刚这段代码:
    在这里插入图片描述
    我们发现0x00 00 00 14在内存中是倒着放的(即14 00 00 00),如果我们想解决这个问题,我们就需要知道大小端的概念。

    什么是大端小端

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

    在这里插入图片描述

    为什么会有大端和小端:

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

    设计程序判断机器大小端

    现在我们写一段代码来告诉我们当前机器的字节序是什么。
    2

    在这里插入图片描述
    现在我们把这段程序分装成一个函数:

    #include
    int check_sys()
    {
    	int a = 1;
    	return *(char*)&a;
    }
    int main()
    {
    	int ret = check_sys();
    	if (ret == 1)
    	{
    		printf("小端\n");
    	}
    	else
    	{
    		printf("大端\n");
    	}
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里有必要在回顾一下:指针类型决定了指针在进行解引用操作时能过访问几个字节。

    我们先以int*类型的指针为例:
    在这里插入图片描述
    我们可以看到int*类型的指针在进行解引用操作时可以访问4个字节的大小。
    下面我们在举一个char*类型的指针:
    在这里插入图片描述
    这里我们依然可以发现char*类型的指针在进行解引用是只能访问一个字节。
    再次强调指针类型决定了指针在进行解引用时可以访问空间的大小。

    练习1

    //以下程序输出什么
    #include
    int main()
    {
    	char a = -1;
    	signed char b = -1;
    	unsigned char c = -1;
    	printf("a=%d,b=%d,c=%d\n", a, b, c);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    #include
    int main()
    {
    	char a = -1;
    	//补码:11111111111111111111111111111111
    	//当我们要把-1的二进制序列存放到a中去,a只能存放8个比特位
    	//故a中存放的是11111111
    	//a整型提示后:11111111111111111111111111111111
    	signed char b = -1;
    	//补码:11111111111111111111111111111111
    	//当我们要把-1的二进制序列存放到b中去,b也只能存放8个比特位
    	//故b中存放的是11111111
    	//b整型提示后:11111111111111111111111111111111
    	unsigned char c = -1;
    	//c中存放的是11111111
    	//c整型提示后:11111111111111111111111111111111
    	//虽然a、b、c中存放的内容都是11111111,但是当我们再把它们拿出来的时候就不一样了。
    	//如果要把a、b、c的整型打印出来,那我们就需要计算出a、b、c的整型形式,
    	//所以就需要对a、b、c进行整型提升(整型提升按照原符号进行提升,无符号数进行整型提升时补0)
    	printf("a=%d,b=%d,c=%d\n", a, b, c);
    	//-1  -1  255
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    练习2

    //以下程序输出什么
    #include
    int main()
    {
    	char a = -128;
    	printf("%u\n", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    //以下程序输出什么
    #include
    int main()
    {
    	char a = -128;
    	//10000000000000000000000010000000---原码
    	//11111111111111111111111101111111---反码
    	//10000000000000000000000010000000---补码
    	//10000000
    	//由于要打印无符号十进制数字,故需要对10000000进行整型提升
    	//整型提升之后:11111111111111111111111110000000---这依然是补码
    	//按照正常道理我们需要把补码转换为原码来获得最终的结果,但注意这里是%u,即按照无符号数来进行打印
    	//所以说计算机认为内存中存放的是一个无符号数,既然是这样的或,原、反、补码是相同的
    	//所以直接把11111111111111111111111110000000拿出来充当我们的原码
    	//最终结果为:4294967168
    	printf("%u\n", a);//%u是打印十进制的无符号数字
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    char类型的范围是如何定义的

    在这里插入图片描述
    在这里插入图片描述

    练习3

    //以下输出结果是什么
    #include
    int main()
    {
    	char a = 128;
    	printf("%u\n", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里的变量achar类型的,所以根本存不下128,这里的128就是127+1,即127+1其实就是练习2中的-128,所以说这里变量a存放的就是-128,因此输出结果和练习2一样。即:
    在这里插入图片描述

    注意:把练习2和3对比着进行加深理解。

    练习4

    //以下输出结果是什么
    #include
    int main()
    {
    	int i = -20;
    	unsigned int j = 10;
    	printf("%d\n", i + j);
    	//可以按照补码的形式进行运算,最后格式化成整数
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    未来再遇到这种题时不要害怕,大不了不就是拿起笔来算一算嘛😀

    练习5

    //以下输出结果是什么
    #include
    int main()
    {
    	unsigned int i;
    	for (i = 9; i >= 0; i--)
    	{
    		printf("%u\n", i);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    打印时我们打印的是i,i又通过%u来打印,所以打印出来的是无符号的整数,不管是一个什么样的数字都会当作无符号数(永远>=0)来进行打印,打印出来的都是>0的数字,即使是一个负数进去,站在i的角度它都会认为是一个正数。所以说结果是一个死循环。
    当i减到-1、-2、-3、-4…时,-1、-2、-3…的补码被当作整数的补码进行输出。这一点尤其要注意

    练习6

    //以下输出结果是什么
    #include
    #include
    int main()
    {
    	char a[1000];
    	int i;
    	for (i = 0; i < 1000; i++)
    	{
    		a[i] = -1 - i;
    	}
    	printf("%d\n", strlen(a));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    -1、-2、-3、-4…-998、-999、-1000这1000个数字如果真的存放到数组a中,任何一个数字放到数组a中去都会转换为-128->127之间的一个数字。
    在这里插入图片描述
    还记的这个圆吗,当i从-128到-129时我们发现又循环回去了,-128-1得到的结果时正的127,不可能得到-129.
    数组a中存放的数:-1、-2、-3、-4…-128、127、126、125…2、1、0、-1、-2、-3…。所以程序最终运行的结果为255。即:
    在这里插入图片描述
    还要注意一个点:数字0和字符’0’不是一回事。\0其实就是数字0其对应的ASICC码值是0;而字符’0’对应的ASICC码值是48。

    练习7

    //以下输出结果是什么
    #include
    unsigned char i = 0;
    int main()
    {
    	for (i = 0; i <= 255; i++)
    	{
    		printf("hello world\n");
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里的char类型是unsigned char,unsigned char类型存放的数的范围是0->255,而判断中的i<=255这个条件就恒成立,所以程序结果依然是死循环。
    此题可以与练习5进行比较学习。
    所以我们今后再使用无符号数时,如果条件把握的不合适,判断其是不是大于小于等于某个数时,极有可能导致程序死循环,无符号数很容易导致程序陷入死循环。在今后写代码过程中一定要特别注意无符号数。
    至此练习题到此结束,这7道练习题跟数据在内存中的存储有着非常大的关系。一定要把这7道题理解透。

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

    常见的浮点数:

    3.14159 1E10(1.0*10^10)
    浮点数家族:float 、double、 long double类型。

    浮点数存储的例子:

    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    再继续往下看之前,请务必思考一下上面的举例,想一想程序运行的结果是什么?
    下面请看程序运行结果:
    在这里插入图片描述
    程序结果是不是和读者想的是一样的呢?如果是,那恭喜读者对浮点数在内存中的存储有着不错的理解,倘若不是的话,别灰心,接下来我会逐步的对这道题进行分析:
    我们首先要知道整型和浮点型在内存中存储的方式是截然不同的。对于整型,其在内存中是以二进制序列的补码进行存储的(涉及到大端小端);那浮点数在内存中是怎样存储的呢?
    请看:
    根据国际标准IEEE(电气和电子工程协会)规定,任何一个浮点数NUM的二进制数可以写为:
    NUM = (-1) ^ S * M * 2 ^ E
    (这里S表示符号,E表示阶乘,M表示有效数字)
    ①当S为0时,表示一个正数;当S为1时,表示负数
    ②M表示有效数字,1<= M <2
    ③2^E表示指数

    (-1) ^ S意思就是你要表示的数是一个正数还是一个负数,它就相当于一个符号位。
    那M为什么1<= M <2呢?是这样的,科学计数法表示的二进制里是不可能出现2这个数的,所以我们要把它化成有效数字时就是1点多少多少。
    2^E表示的是指数位

    我们就拿9.0进行举例,可以表示为:

    (-1)^0 *1.001 * 2^3
    这里S=0 M=1.001 E=3
    (-1) ^ S * M * 2 ^ E

    所以浮点数在内存中存储的话我们只需要把S M E这些值存起来,我们就可以还原回来真实的浮点数是几。真正的IEEE754规定的确实是我们只要把S M E相关的一个值存放到内存中去,回头我们在回复回来就可以。但举例到底是怎么存储的呢?假设我们把一个浮点数首先由十进制写成二进制,之后再把这个二进制写成科学计数法即(-1) ^ S * M * 2 ^ E的表示形式,求出S M E的时候,这个时候我们再把S M E相关的值存起来,怎么存的呢?请看:
    IEEE 754规定:对于32位(32个比特位)的浮点数,最高的一位是符号位S,接着的8位是指数E,剩下的23位是有效数字M
    在这里插入图片描述
    对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
    在这里插入图片描述
    在这里插入图片描述

    既然把S M E划分的三块空间之后,是不是把这些值直接扔到里面去就可以呢?当然不是这样的。请接着往下看:
    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。
    举个例子

    0.5
    (-1)^0 * 1.0 *2^(-1)
    S=0
    M=1.0
    E=-1
    E+127=126(意思是虽然我们计算出来的结果为-1,但我们真正存放到内存中的那个值是126,真实值要减去127,所以倒着推过去可以得到真实值为-1。)

    现在,我们重现整理一下思路:我们把一个十进制的浮点数首先写成一个二进制的浮点数,然后把它转换成科学计数法的表现形式,此时就有的M S E。S直接存进去就好了;M要去点小数点前面的1然后存进去;至于E要给它加一个中间值之后给它存进去。
    我们用代码来简单验证一下:
    在这里插入图片描述

    .
    .
    .
    以上我们是讨论是如何放进去,而指数E从内存中往外取的时候又分为三种情况
    E不为全0或不为全1

    这是,浮点数就采用下面的规则表示,即指数Education计算值减去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位0000000000000000000000000000,则其二进制表示形式为:
    0 01111110 0000000000000000000000000000

    E为全0

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

    E为全1

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

    好了,关于浮点数的表示规则就说到这里。

    现在我们回到之前未讲解的题目😜

    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    #include
    int main()
    {
    	int n = 9;
    	//0 00000000 00000000000000000001001-补码
    	float* pFloat = (float*)&n;
    	printf("n的值为:%d\n", n);//9
    	printf("*pFloat的值为:%f\n", *pFloat);//0.000000
    	//(-1)^0 * 0.00000000000000000001001 * 2^(-126)
    
    	*pFloat = 9.0;
    	//1001.0
    	//1.001*2^3
    	//(-1)^0 * 1.001 * 2^3   这里E为3,存的时候要加上127变为130,即10000010
    	//0  10000010  00100000000000000000000这就是9.0存的内存的形式
    
    	printf("num的值为:%d\n", n);//这个时候要打印n的话,
    	//既然要打印n(n是一个整数)就要站在n的角度,认为内存中存的是一个整数的补码,
    	//即01000001000100000000000000000000,这也是这个整数的原码
    	//01000001000100000000000000000000转换为十进制为1091567616
    	printf("*pFloat的值为%f\n", *pFloat);//9.0
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在这里插入图片描述
    到这里相信大家对浮点数在内存中的存储有了一定的了解。原来是浮点数的形式放进去和以整型的形式放进去的方式是不一样的,取出来的方式也不一样。所以,整型的形式放进去再以浮点型的形式取出来得到的结果肯定是不正确的,同理以浮点型的形式放进去再以整型的形式取出来得到的结果肯定也是不正确的。所以,浮点数就应该按照浮点数的形式存,按照浮点数的形式取;整型就应该按照整型的方式存,按照整型的方式取。
    之所以我们要了解数据在内存中的存储是因为,在未来我们遇到某些特殊的值比较怪异时,我们有能力去分析它到底结果是怎样的

    到这里,整个数据在内存中的存储的讲解就结束了。
    本文至此结束,感谢各位🙇‍。

  • 相关阅读:
    ResizeObserver loop completed with undelivered notifications.
    孪生神经网络
    HTML学生个人网站作业设计成品 HTML+CSS肖战明星人物介绍网页 web结课作业的源码...
    生活中常见的嵌入式产品都有哪些?
    【错误:No package snapd available.】在 CentOS 上启用 snap 并安装 snapd
    一文详解TCP三次握手四次挥手
    Reflect 对象的创建目的
    美术作品的著作权
    Open GOP 和close GOP
    Aiomysql 与 Sqlalchemy 的使用
  • 原文地址:https://blog.csdn.net/m0_74352571/article/details/127748569