• 【C语言从青铜到王者】第五篇·数据在内存中的存储


    本篇前言
    从本篇开始,我们要开始逐渐和内存打交道了。想学好C语言,打牢编程基本功,我们心中一定要时刻有内存的概念。

    数据类型及其意义

    整型与浮点型

    char

    short

    int

    long

    long long

    float

    double

    构造(自定义)类型

    数组类型

    结构体类型 struct

    枚举类型 enum

    联合类型 union

    空类型

    void

    void test (void) 函数返回类型 函数参数

    void* p 指针

    指针类型

    以上所有类型 *

    数据类型的意义

    数据类型的意义有两个:

    1.决定为变量开辟内存空间的大小

    2.决定看代内存中0/1序列的视角

    这两句话到底是什么意思,相信看完本文你就清楚了


    整型数据在内存中的存储

    整型家族

    char

    unsigned char signed char

    short

    unsigned short signed short

    int

    unsigned int signed int

    long

    unsigned long signed long

    为什么char类型是整型家族呢?

    因为字符型数据是按照ASCII码值存储在内存中的,而ASCII码值也是整数,所以char类型也是整型家族的一员

    整型数据在内存中的存储

    任何数据在内存中都是以二进制序列存储。整数的二进制序列有三种形式,分别是原码、反码、补码

    整数在内存中是以补码的形式存储的,比如看下面的a:

    #include
    int main()
    {
    	int a = -10;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述
    在这里插入图片描述
    为什么-10在内存中存储为f6 ff ff ff的形式呢?

    • 详解:
      -10的原码:
      10000000 00000000 00000000 00001010
      -10的反码:
      11111111 11111111 11111111 11110101
      -10的补码:
      11111111 11111111 11111111 11110110
      换算成16进制:
      ff ff ff f6

    为什么内存中存补码而不存原码?

    比如我们想计算 1 - 1这个算数

    由于计算器底层没有减法器,我们需要用加法器代替减法器

    1 - 1 → 1 + (-1)

    用原码:

    1 :00000000 00000000 00000000 00000001

    -1 :10000000 00000000 00000000 00000001

    相加:10000000 00000000 00000000 00000010

    结果是 -2 ,不符合结果

    用补码:

    1 : 00000000 00000000 00000000 00000001

    -1 :11111111 11111111 11111111 11111111

    相加:1 00000000 00000000 00000000 00000000

    由于只有32位,首位丢弃:

    结果:00000000 00000000 00000000 00000000

    结果为0,符合结果

    出现这一现象的本质原因是

    1.补码可以将二进制数据的符号位和数值位统一处理,可以将加法和减法统一处理

    2.补码和原码的相互转换,其运算过程是相同的,不需要额外的电路(原码→取反+1→补码 、补码→取反+1→原码)

    这也是为什么密码学家发明补码的一个原因


    大小端字节序

    刚刚-10的例子的结果同学们一定有疑问:

    -10的16进制补码:ff ff ff f6

    而编译器中内存值:
    在这里插入图片描述
    为什么顺序正好反过来了呢?

    这就涉及到了大小端字节序的问题

    我们把一个内存单元看成一个整体,1字节8bit位,8位二进制即二位16进制,所以两个16进制的数字就表示一字节的大小,也就是一个内存单元的大小。而将这些单元编号(也就是标上地址)的顺序是可以不同的:

    大端字节序:高位数字放在高地址(符合我们的阅读习惯)

    小端字节序:高位数字反而放在低地址(我的编译器的内存中存储类型)

    为什么有大小端?

    内存中,每个地址单元都对应一个内存单元,大小为1字节8bit。如果存储的类型都是8bit大小,也就不需要对内存单元进行排序,但是C语言中还有16bit的short类型,32bit的int类型等等超过一个内存单元大小的数据类型,现在流行的32位64位处理器的寄存器宽度也大于一个字节,所以我们不得不面临将多字节安排的问题

    我写的判断自己编译器大小端的代码:

    #include
    int main()
    {
    	int a =1;
    	char* p = &a;
    	if (*p)
    		printf("小端");
    	else
    		printf("大端");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    image.png

    实例操练

    题一:请手算下面的程序的输出结果

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

    在这里插入图片描述

    • 详解:
      -1的补码:
      11111111 11111111 11111111 11111111
      由于a、b、c三个变量都是一字节
      截断:
      11111111
      则a、b、c变量位置内存值为:
      11111111
      a、b为有符号char,所以最高位为符号位(char是有符号还是无符号,是由编译器决定的,但是大部分编译器是有符号)
      1 1111111
      由于是以%d打印,需要进行整型提升
      a、b整型提升后补码:
      11111111 11111111 11111111 11111111
      打印出原码的十进制:
      -1
      c为无符号char,所以所有位都是数值位
      c 整型提升后补码:
      00000000 00000000 00000000 11111111
      原码:
      00000000 00000000 00000000 11111111
      十进制:
      255

    题二:请手动计算下面的程序的输出结果

    #include
    int main()
    {
    	char a = -128;
    	printf("%u", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    • 详解:
      -128的二进制原码:
      10000000 00000000 00000000 10000000
      反码:
      11111111 11111111 11111111 01111111
      补码:
      11111111 11111111 11111111 10000000
      放入char中发生截断:
      10000000
      由于%u打印,所以整型提升,且无符号位
      11111111 11111111 11111111 10000000
      由于无符号数的原反补码相同
      在这里插入图片描述

    总结:

    1.数值是以补码在内存中操作(截断、整型提升)的

    2.printf中的%d等类型决定的是最后看待内存中补码的角度和是否需要整型提升

    题三:请手动计算下面的程序的输出结果

    #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
    • 详解:
      -20的二进制序列
      10000000 00000000 00000000 00010100
      反码
      11111111 11111111 11111111 11101011
      补码
      11111111 11111111 11111111 11101100
      10的二进制序列
      原反补码
      00000000 00000000 00000000 00001010
      i + j 补码相加
      11111111 11111111 11111111 11110110
      反码
      11111111 11111111 11111111 11110101
      原码
      10000000 00000000 00000000 00001010
      %d为有符号整型,结果为
      -10

    题四:请手动计算下面的程序的输出结果

    #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

    感性的定性判断一下:i是无符号数,无符号数一定大于等于0,所以代码会死循环

    • 详解:
      9的二进制序列
      00000000 00000000 00000000 00001001
      以u%看待,就是9
      同理程序依次打印出87654321
      i = 0时,打印出0
      00000000 00000000 00000000 00000000
      然后i--
      11111111 11111111 11111111 11111111
      由于是无符号数,所以原反补码相同
      它的十进制为:
      232-1
      所以这整个循环就是:
      232-2232-310232-1232-2…(死循环)

    题五:请手动计算下面的程序的输出结果

    #include
    #include
    int main()
    {
    	char a[1000];
    	int i;
    	for (i = 0; i < 1000; i++)
    	{
    		a[i] = -1 - i;
    	}
    	printf("%d", strlen(a));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    image.png

    • 详解:
      -1的二进制序列补码
      11111111 11111111 11111111 11111111
      放入a[0]截断
      11111111
      原码:
      10000001
      a[0]是-1
      -2的二进制补码
      11111111 11111111 11111111 11111110
      截断后原码
      10000010
      a[1]是-2
      一直到-127
      原码
      10000000 00000000 00000000 01111111
      反码
      11111111 11111111 11111111 10000000
      补码
      11111111 11111111 11111111 10000001
      截断后
      10000001
      原码
      11111111
      a[126]是-127
      -128
      原码
      10000000 00000000 00000000 10000000
      补码
      11111111 11111111 11111111 10000000
      截断
      10000000
      原码的值为-128(特殊序列)
      -129
      原码
      10000000 00000000 00000000 10000001
      补码
      11111111 11111111 11111111 01111111
      截断
      01111111
      原码的值为127
      循环为
      -1 → -128 → 127 → 0
      当a为0时候,就是strlen的结束标志
      所以前面有128+127一共255个元素

    char类型存储的数据范围

    内存中补码:00000000 → 11111111

    正数:00000000 → 01111111 即 0 → 127

    负数:11111111 → 1000001 即原码 10000001 → 11111111 即 -1 → -127

    特殊补码序列 10000000 由于数值位无法再减去1 所以直接规定 10000000 的原码值为 -128

    因为-128本身的原码:
    10000000 00000000 00000000 10000000
    反码:
    11111111 11111111 11111111 01111111
    补码:
    11111111 11111111 11111111 10000000
    装入char类型中发生截断:
    10000000
    也就是特殊序列10000000
    所以char的取值范围是 -128 → 127


    浮点型数据在内存中的存储

    浮点型家族

    float

    double

    常见的浮点数:

    3.1415926

    1E10 (1×10^10)

    引例

    #include
    int main()
    {
    	int n = 9;
    	float* pFloat = (float*)&n;
    	printf("n的值为:%d\n", n);
    	printf("pFloat的值为:%f\n", *pFloat);
    
    	*pFloat = 9.0;
    	printf("n的值为:%d\n", n);
    	printf("pFloat的值为:%f\n", *pFloat);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    为什么会出现的这样的结果呢?
    第一个n的值我们很熟悉,存进去正整数9,打印出正整数9
    但二、三、四的结果就很让人匪夷所思了
    下面就来解析为什么会出现这样的结果


    浮点型数据是怎么放入内存的

    浮点数在内存中的存储是由IEEE(电气电子工程协会)的754标准规定的:

    任意一个浮点数可以表示成

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

    (-1)^s 表示符号位 s=1就是负数 s=0就是正数

    M表示有效数字 1 <= M < 2

    E表示指数位

    下面详细说一下这是啥意思。
    首先我们要学会将小数转换成二进制数。
    我们以前都学过科学计数法,这种计算小数的方法其实就是用2作为底数的科学计数法
    举例说明怎么转换:
    现有浮点数 十进制表示法为 5.5
    5的二进制:101(整数除以2,结果的余数作为每一次的结果,除数再除以2,直到除数为0)
    0.5的二进制:0.1(小数乘以2,结果的整数位作为每一次的结果,小数再乘以2,直到小数为0)
    image.png

    所以5.5的二进制表示就是101.1

    科学计数法就是 1.011×2^2

    则5.5表示成 (-1)^s * M * 2^E 就是

    (-1)^0 * 1.011 * 2^2

    s=0

    M=1.011

    E=2

    拿到了这三个参数后,我们看看它们是怎么存入内存的

    754规定:

    对于float类型,一共分配4字节32bit内存

    第1位为s位,第2-9位 8bit 为 E,第10-32位 23bit 为M

    对于double类型,一共分配8字节64bit内存

    第1位为s位,第2-12位 11bit 为 E,第13-64位 52bit 为M

    S的值:正数放0 负数放1 与整数的符号位意义相同

    M的值:由于M一定是1.xxxxxx的形式,所以1可以不存,只存后面的数字(为了增加存储的有效数字量,增加精度),后面的数字直接顺序排列在M的位置上

    E的值:由于E是整数,可正可负 8bit的范围是0-255,11bit的范围是0-2047,所以E的值必须在原来整数的基础上加上127和1023,这样E表示的范围就是-127 → 128和-1023 → 1024

    综上,可以知道float和double表示的数字范围

    image.png

    紧接上文,5.5的三个参数为
    s=0
    M=1.011
    E=2
    拿到了这三个参数后,经过处理:
    s=0,E=2+127=129=10000001,M=011
    所以总序列为
    0 10000001 01100000000000000000000
    再把二进制换成16进制
    0100 0000 1011 0000 0000 0000 0000 0000
    40 b0 00 00
    再按照小端字节序排序
    00 00 b0 40
    来见证奇迹吧:

    image.png

    image.png
    实验证明浮点型数据确实是这样存储的


    浮点型数据是怎么从内存中拿出来的

    我们已经知道了浮点型数据是怎么放入内存中,现在来探讨浮点型数据是怎么拿出来的

    浮点数的取出,其实就是存入的反操作,但是有以下不同的情况

    情况一:E全为0

    指数E的真实值为-127或-1023,此时的浮点数无限接近于0,此时M的值还原时不再+1,表示为很小的数字,显示出来就是0

    情况二:E全为1

    表示±无穷大

    情况三:E不全为0且不全为1

    E-127得到E的真实值 M+1得到M的真实值 最后得到真实的浮点数

    现在终于可以把引例重新拿过来看看:

    #include
    int main()
    {
    	int n = 9;
    	float* pFloat = (float*)&n;
    	printf("n的值为:%d\n", n);
    	printf("pFloat的值为:%f\n", *pFloat);
    
    	*pFloat = 9.0;
    	printf("n的值为:%d\n", n);
    	printf("pFloat的值为:%f\n", *pFloat);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YM6BwtN6-1661741469783)(C:\Users\23867\AppData\Roaming\Typora\typora-user-images\image-20211001164938306.png)]

    9的二进制位:
    0000000 0000000 0000000 00001001
    按浮点型存储划分
    0 00000000 00000000000000001001
    可见E为全0,符合情况一,所以打印出来是0.000000(后面的位数显示不出来)
    而9.0的二进制位:
    1.001×2^3 s=0 E=130 M=001
    0 10000010 00100000000000000000000
    再以有符号整型的十进制读出

    image.png

    这样第三个结果也就出来了

    至此,上面程序结果的解析也就全部完成,浮点数在内存中的存储也讲解完毕


    回顾一下,本文重点讲解了整型和浮点型数据在内存中的存储形式。现在我们知道这些数据是如何被存放在内存中的了。从本文可以看出,计算机和人脑的思维方式差别还是很大的,当“人脑”用“电脑”的方式思考问题是不是多少有点“烧脑”呢?

  • 相关阅读:
    【ElasticSearch 集群】Linux安装ElasticSearch集群(图文解说详细版)
    输电线路故障诊断(利用随机森林方法实现二分类和多分类)
    关于Elasticsearch(es)中使用sql返回结果只有1000条
    不理解路径问题的大坑记录
    《MySQL 数据库(二)》
    Numpy:打开通往高效数值计算的大门
    基于神经网络的帧内预测和变换核选择
    uni-app小程序开发使用uView,u-model传入富文本内容过长,真机上无法滚动
    DNA 10. 识别癌症驱动基因 (OncodriveCLUST)
    HTTP服务器——tomcat的安装和使用
  • 原文地址:https://blog.csdn.net/qq_51379868/article/details/126580463