• 数据的存储(1)


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

    本章重点

    1. 数据类型详细介绍
    2. 整形在内存中的存储:原码、反码、补码
    3. 大小端字节序介绍及判断
    4. 浮点型在内存中的存储解析

    正文开始

    1. 数据类型介绍

    前面我们已经学习了基本的内置类型(C语言本身自带的类型):

    char        //字符数据类型
    short       //短整型
    int         //整形
    long        //长整型
    long long   //更长的整形
    float       //单精度浮点数
    double      //双精度浮点数
    //C语言有没有字符串类型?
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    以及他们所占存储空间的大小。
    1 2 4 4/8 8
    4 8

    类型的意义:

    1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
    2. 如何看待内存空间的视角
      由于数据大小不同,数据类型的范围不同,有了丰富的类型,让我们选择合适的数据类型时更加方便,在一定程度上可以避免内存的浪费。
      其次,数据类型的不同,决定了我们看待数据的角度。
      当是int short我们认为这是一个整数,当看到double float我们认为这是一个小数。

    1.1 类型的基本归类

    整形家族:

    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

    unsigned 无符号的
    int是整形,short是短整型,而short int 是全称,int可以省略。
    每一个字符都有对应的ASCII码值,所以char归类到整形。
    unsigned 和signed
    生活中数值有正数有负数。
    然后有些数值,只有正数,没有负数(身高)
    有些数值,有正数也有负数(温度)

    在C语言中为了把数值描述更加准确,因此把只有正数没有负数的类型,叫做无符号数字。
    有些数值,有正数也有负数,为了区分正负,叫做有符号数字。

    在C语言中并没有明确规定char类型是有符号还是无符号,所以更准确的可以把char类型分为char,signed char,unsigned char。其中char是有符号还是无符号是不能明确的,有些编译器认为char是有符号的,有些编译器认为char是无符号的。

    然而short int long 是有规定给的,如果只写short int long,编译器默认为是有符号的。
    short 等价于signed short
    int 等价于signed int
    long 等价于signed long

    浮点数家族:

    float
    double
    
    • 1
    • 2

    浮点数家族我们暂时不讲。

    构造类型:
    构造类型也叫做自定义类型

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

    为什么数组类型也叫自定义类型?

    char arr[6]   //数组元素类型和数组元素个数由自己决定 
    char arr[5]
    int arr[5]
    
    • 1
    • 2
    • 3

    数组元素类型和数组元素个数由自己决定。所以数组类型我们归为构造类型(自定义类型)。

    结构体类型我们比较了解。
    枚举类型和联合体类型不太了解。
    这三块内容我们放到第四节,自定义类型详解中进行讲解。

    指针类型

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

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

    返回类型

    void test()
    {
      //不需要返回值
    }
    int main()
    {
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    函数的参数

    void test(void)
    {
      //不需要函数的参数
    }
    int main()
    {
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    指针类型

    
    int main()
    {
      void* p = NULL;
      int a = 10;
      void* p1 = &a;  //原本是用int*类型进行存储
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    void* 就像一个垃圾桶,不管什么类型的指针类型,甚至结构体类型的指针都能往里面放。
    虽然什么类型的指针都能往void*里面进行存储。但是却不能进行操作。

    void* = &p;
    p++;  //error
    *p;  //error
    
    • 1
    • 2
    • 3

    void*一般用于临时存放地址,可以通过强制类型转换进行使用,至于它的应用,我们以后再说。

    2. 整形在内存中的存储

    我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
    那接下来我们谈谈数据在所开辟内存中到底是如何存储的?
    比如:

    int a = 20;
    int b = -10;
    
    • 1
    • 2

    我们知道为 a 分配四个字节的空间。
    那如何存储?
    下来了解下面的概念:

    2.1 原码、反码、补码

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

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

    原码
    直接将数值按照正负数的形式翻译成二进制就可以得到原码。
    反码
    将原码的符号位不变,其他位依次按位取反就可以得到反码。
    补码
    反码+1就得到补码。

    举个例子

    int main()
    {  int a = 20;
    //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 补码
    
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    对于有符号的数字来说,最高位是符号位。

    对于整形来说:数据存放内存中其实存放的是补码。
    为了验证数据在内存中是以补码的形式存在。
    打开调试-窗口-内存
    查看b的数据
    f6 ff ff ff 十六进制
    经过计算正好就是对应的二进制补码,不过是倒着进行存储。

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

    cpu只有加法器,如何计算减法?

    int main()
    {
      int c = 1-1;
      //1 + (-1)在计算减法的时候,把减号和后面的数字看成一个整体进行相加。
    //补码相加
    //  00000000 00000000 00000000 00000001
    //  11111111 11111111 11111111 11111111
    //相加为
    //100000000 00000000 00000000 00000000
    由于是int类型,最高位无法存储,所以保留的就是0。
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    原码与补码的转换
    原码到补码,有一种方式,原码取反、加一得到补码
    补码到原码有两种方式
    1 补码减一、取反得到原码
    2 补码取反、加一得到原码
    方式二举例
    -1的
    补码11111111 11111111 11111111 11111111
    10000000 00000000 00000000 00000000
      10000000 00000000 00000000 00000001

    这也就是为什么上面说补码与原码相互转换,其运算过程是相同的。

    int main()
    {
      int a = 0x12345678;
      return 0;
    }
    //在内存窗口显示为
    //78 56 34 12
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    一个十六进制位是四个二进制位,两个十六进制位是八个二进制位。一个字节,八个比特位。所以两个十六进制位是一个字节。所以八个十六进制位刚好四个字节。

    所以是以字节为单位进行存储。

    至于刚刚提到的是什么,下面我们介绍*大小端介绍**

    2.2 大小端介绍

    2.2.1什么大端小端:

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

    2.2.2 为什么有大端和小端:

    个人理解

    内存中的存储单元是一个字节,所以我们以字节讨论存储顺序问题。
    当一个数值超过一个字节了,要存储到内存中,就有顺序的问题。

    0x11223344
    低地址---------->高地址

    11 22 33 44/22 11 33 44 /44 33 22 11
    理论上随意存储,只要最后能有办法还原成0x11223344就可以。

    但是如果随意存储的话,存储的顺序很难记忆,并且不符合常理。
    所以为了简单,方便。
    数据的存储留下两种方法要么
    11 22 33 44 要么44 33 22 11
    这就是所谓的大小端。

    对于一个十进制数字123,3是个位,1是百位,也就是3在低位,1在高位。
    对于0x11223344 44是低字节的数据,11是高字节的数据。
    小端字节序存储就相当于倒着进行存储。
    大端字节序就相当于正着进行存储。

    vs编译器环境下就是小端字节序存储。

    我们常见的x86是小端模式。

    补充:
    我们是以字节为单元讨论数据存储顺序。
    数值有不同的表现形式,10进制,2进制,8进制。
    其中16进制是1-9- a-f组成
    其中a-f代表10-15

    其中f(15)的二进制是1111

    所以对一个二进制数字例如
    1111 0000 1111 1011
    每四位转换为一个十六进制数字。

    所以一个十六进制位代表四个二进制位。两个十六进制是八个二进制位,也就是一个字节。

    补充:

    1内存中存放的是补码
    2整形表达式计算式用的内存中补码计算的。
    3打印和我们看到的都是原码

    百度2015年系统工程师笔试题:
    请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)

    小端字节序存储:
    把一个数值的低位字节的内容存在低地址处,高位字节的内容,存放在高地址处。
    大端字节序存储:
    把一个数值的低位字节的内容存在高地址处,高位字节的内容,存放在低地址处。

    程序:
    对于int a= 1;
    十六进制为0x 00 00 00 01
    如果是小端存储为 01 00 00 00
    如果是大端存储为 00 00 00 01
    判断首元素地址指向一个字节的元素是0或者1就好了

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

    如果将代码封装成函数

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    对代码进行简化

    int check_sys()
    {
      int a = 1;
      if(*(char*)&a==1)
      {
        return 1;
      }
      else
      {
        return 0;
      }
    }
    int main()
    {
      if(check_sys()==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 check_sys()
    {
      int a = 1;
      return *(char*)&a;  //返回1表示小端,返回0表示大端
    }
    int main()
    {
      if(check_sys()==1)
        printf("小端\n");
      else
        printf("大端\n");
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2.3 练习

    代码一

    //输出什么?
    #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;
    }
    //-------------------
    //编译器运行结果为
    //a=-1,b=-1,c=225
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    先看char类型的-1存储
    先写出来-1的原码反码补码
    10000000 00000000 00000000 00000001 -1的原码
    11111111 11111111 11111111 11111110 -1的反码
    11111111 11111111 11111111 11111111 -1的补码
    但是由于存储到char类型中,只有8个比特位,所以会发生截断。只把最右边八个1进行存储
    11111111

    再看signed char 类型-1的存储。
    11111111 11111111 11111111 11111111 -1的补码
    但是由于存储到char类型中,只有8个比特位,所以会发生截断。只把最右边八个1进行存储
    11111111

    再看unsigned char 类型-1的存储。
    11111111 11111111 11111111 11111111 -1的补码
    但是由于存储到char类型中,只有8个比特位,所以会发生截断。只把最右边八个1进行存储
    11111111

    虽然存储的一样,但是char 和signed char 把最高位当成符号位,unsigned char的八个比特位都是数值位。
    打印的时候是%d ,对应的是整形,而我们是char类型,需要发生整形提升。
    char a存储的是11111111进行整形提升。
    整形提升按照符号位补充。
    整形提升完为
    11111111 11111111 11111111 11111111这是补码
    11111111 11111111 11111111 11111110这是反码
    10000000 00000000 00000000 00000001这是原码

    所以打印出来是-1
    因为char 在vs中等价于signed char
    所以signed char 打印出来也是-1

    对于unsigned中存储的11111111
    先进行整形提升,由于是无符号的,所以直接补充0
    00000000 00000000 00000000 11111111 这是补码
    00000000 00000000 00000000 11111111 这是原码
    所以打印出来是225

    补充
    16进制打印 %x
    8进制打印 %o
    有符号数和无符号数的取值范围如何定?
    有符号位
    一个字节,八个比特位,所能存储的所有数字
    00000000 0
    00000001 1
    00000010 2
    00000011 3
    00000100 4
    ···
    01111111 127
    10000000 -128
    10000001 -127
    ···
    11111110 -2
    11111111 -1

    2^8是256
    其中对于10000000直接规定为-128
    因为-123的二进制数字为
    原码110000000
    反码101111111
    补码110000000
    存储到8个比特位中,就是10000000

    所以char 和signed char的取值为-128~127

    无符号
    8个比特位都是数值位
    11111111就是255
    所以对于一个unsigned char来说,范围是从0~255

    由此类推
    signed short
    0000000000000000 0
    ···
    0111111111111111 32767
    1000000000000000 -32768
    ···
    1111111111111111 -1

    所以signed short 的取值范围是-32768~32767

    unsigned short 的取值范围是 0~65535

    代码二

    2.
    #include 
    int main()
    {
        char a = -128;
        printf("%u\n",a);
        return 0;
    }
    //-----------------
    //编译器运行结果为
    //4294967168
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    -128对应的32位比特位
    10000000 00000000 00000000 10000000原码
    11111111 11111111 11111111 01111111反码
    11111111 11111111 11111111 10000000补码
    存放在char a 中的数据为
    10000000
    将%u打印则进行整形提升,至于整形提升补0还是1看数据存放的数据类型,而不是要打印的格式
    整形提升
    因为有符号,就补充最高位
    11111111 11111111 11111111 10000000
    所以这个就是整形提升后的补码
    再回归原码
    因为打印的为无符号的%u,所以原码就是补码
    11111111 11111111 11111111 10000000打印的十进制数字为4294967168

    这个就是我们今天讲的关于整形存储还有大小端的问题,不过我们的浮点数存储还没有讲解,留到下次再分享。

  • 相关阅读:
    echarts-雷达图和仪表图
    python基于GDAL的多线程高速批量重采样、对齐栅格、对齐行列数,并无损压缩
    python---协程与任务详解
    伪类中使用element图标
    攻防世界web篇-cookie
    【XGBoost】第 1 章:机器学习前景
    Android EditText筛选+选择功能开发
    CEC2018:动态多目标测试函数DF6~DF9的PS及PF
    [Python从零到壹] 五十.图像增强及运算篇之图像直方图理论知识和绘制实现
    Python:函数篇(每周练习)
  • 原文地址:https://blog.csdn.net/m0_71545436/article/details/128098322