• 从零开始C语言精讲篇7:数据的存储



    前言

    本章重点:

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

    一、数据类型详细介绍

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

    ps:字符类型char在底层存储的是ASCII码值,所以我们往往把char划分到整形家族里

    类型的意义:

    1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
    2. 如何看待内存空间的视角。

    二、类型的基本归类:

    2.1整形家族:

    char
     unsigned char//无符号字符型
     signed char//有符号字符型
     //char 类型默认是有符号还是无符号是取决于编译器的,大部分编译器默认是有符号的
    
    short
     unsigned short [int]//[int]表示这个int可以不写, unsigned short= unsigned short int
     signed short [int] //默认情况都是有符号的,signed short=short
    
    int
     unsigned int
     signed int//默认情况都是有符号的,signed int=int
    
    long
     unsigned long [int]
     signed long [int]//默认情况都是有符号的,signed long=long
    
    long long
     unsigned long long [int]
     signed long long [int]//默认情况都是有符号的,signed long long=long long
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    关于unsigned:

    #include
    int main()
    {
    	unsigned char c1 = 255;
    	printf("%d\n", c1);
    
    	char c2 = 255;
    	printf("%d\n", c2);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述
    解释如下:
    我们知道一个char类型是1字节,也就是8比特位,255转换成2进制是1111 1111

    如果是unsigned char,就说明是无符号的char,也就是最高位是有效位,那么这个值就不存在原反补的概念了,类似非负数,原反补一样,内存里存的1111 1111打印出来也是1111 111这个值(我们转换成10进制是变成了255)

    如果是char,也就是有符号的char,最高位是符号位,这个就是有原反补码的
    补:1111 1111
    反:1111 1110 (反码=补码-1)
    原:1000 0001(原码=反码符号位不变,其他位全部改变)
    我们存储的是补码,打印的是原码,1000 0001(有符号)转换成10进制就是-1

    注:无符号char取值范围是0~255
    有符号char取值范围是-128~127

    2.2浮点数家族:

    float
    double
    
    • 1
    • 2

    构造类型:

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

    结构体类型详解链接
    枚举类型详解链接
    联合体类型详解链接

    关于数组类型,这个知识点很简单,
    我以int型数组给大家举个例子,推广到其他类型数组也是一样的

    int main()
    {
    	int arr[10] = { 0 };//数组类型就是把数组名去掉,剩下的类型int [10]
    	printf("%d",sizeof(int[10]));//打印40,因为整形int大小为4,一共有10个整形,4*10=40
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    2.3指针类型

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

    指针详解链接

    2.4空类型:

    void//一般用于函数返回值类型,也就是不需要返回值的情况
    
    • 1

    ps:如果你哪天突发奇想想测一下sizeof(void),你会发现,会报错

    三、整形在内存中的存储

    3.1引子

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

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

    我们对上面代码进行调试,然后监视内存,发现a的地址上存储的是8个f
    在这里插入图片描述
    我们这里一个整形是4字节,然后对应32位比特位(二进制)

    f是十六进制的表示,4个二进制才能表示1个十六进制数,
    比如1111表示f(他们都对应十进制的15)

    那么32个二进制数就可以用8个十六进制表示
    ff ff ff ff也就是11111111 11111111 11111111 11111111
    也就是我们常说的补码

    3.2原码、反码、补码

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

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


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

    反码
    将原码的符号位不变,其他位依次按位取反就可以得到了。

    补码
    反码+1就得到补码。

    正数的原、反、补码都相同。
    对于整形来说:数据存放内存中其实存放的是补码。
    我们以%d打印出来的,那个是原码

    还是看这段代码:

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

    a=-1
    -1的原码:10000000 00000000 00000000 00000001
    -1的反码:111111111 111111111 111111111 111111110(反码=原码符号位不变,其他按位取反)
    -1的补码:111111111 111111111 111111111 111111111(补码=反码+1)
    这个补码也就是我们上面说的ff ff ff ff

    到这块,我们知道了,计算机里整形存的是补码,但是为啥是补码呢?我直接存原码不行吗?
    在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理; 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

    下面是原反补互相得到的方式
    在这里插入图片描述
    除了上面这种方式,补码想得到原码,也可以直接按位取反,然后再加一
    在这里插入图片描述

    举个例子:
    -1的补码为 11111111 11111111 11111111 11111111
    符号位不变,其他位按位取反 1000000 0000000 0000000 0000000
    最后+1得 1000000 0000000 0000000 0000001,也就是-1的原码

    3.3大小端问题

    int main()
    {
        //8个16进制数字正好占32位比特位
    	int a = 0x11223344;//0x表示这是一个16进制数字
    	//11是该数字的高位,44是该数字的低位!
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述
    可以看到,我们这个编译器是把低地址内容放到了低地址处,高地址内容放在高地址处
    我们称它为:小端字节序

    在这里插入图片描述
    还有一种放法是:低地址内容放到了高地址处,高地址内容放在低地址处
    我们称它为:大端字节序
    在这里插入图片描述

    快速记忆:低位地址放低地址就是小端,否则就是大端。你记住“低-低-小”即可

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

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

    3.4整形提升与有/无符号数

    下列代码输出什么?
    例1:

    #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

    在这里插入图片描述
    大部分编译器char都是默认signed char的,笔者这个编译器也是。
    所以我们这里只要解释一下为啥char a=-1和unsigned char c=-1即可

    -1是一个整形,它的原反补如下:
    原码:00000000 00000000 00000000 00000001
    反码:011111111 111111111 111111111 111111110
    补码:011111111 111111111 111111111 111111111

    但是把-1赋值给a时,由于a是char类型,所以存储时会截断变成111111111
    同理,c是unsigned char类型,存储时也会截断变成111111111

    我们打印时,是以%d的形式打印,是打印有符号整形

    所以char类型a在打印的时候,会整形提升,会由1字节(8比特位)变成4字节(32位比特位)
    整形提升:
    如果是无符号数,则高位直接补0;
    如果是有符号数,则高位全补符号位。

    a是有符号数,高位补符号位
    a的补码:111111111 111111111 111111111 11111111
    a的反码:111111111 111111111 111111111 11111110
    a的原码:10000000 00000000 00000000 0000001
    转换成10进制也就是-1

    由于c是无符号数,高位补0
    c的补码:00000000 00000000 00000000 11111111
    然后我们无符号数,是认为和非负数一样的,也就是原反补码相同
    c的原码:00000000 00000000 00000000 11111111
    转换成10进制也就是255

    例2:

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

    -128是一个整形,它的原反补如下:
    原码:10000000 00000000 00000000 10000000
    反码:111111111 111111111 111111111 011111111
    补码:111111111 111111111 111111111 10000000

    -128赋值给char类型的a,存储时会发生截断变成10000000

    我们的a是char类型,是有符号的数,发生整形提升,高位补符号位
    补码:111111111 111111111 111111111 10000000

    我们打印时,是以%u的形式进行打印,是打印无符号整形(非负数原反补相同)
    原码:111111111 111111111 111111111 10000000
    转换成10进制是下面这个数字
    在这里插入图片描述

    例3:

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

    128是一个整形,它的原反补如下:
    原码:00000000 00000000 00000000 10000000
    反码:01111111 111111111 111111111 011111111
    补码:01111111 111111111 111111111 10000000

    -128赋值给char类型的a,存储时会发生截断变成10000000

    我们的a是char类型,是有符号的数,发生整形提升,高位补符号位
    补码:111111111 111111111 111111111 10000000

    我们打印时,是以%u的形式进行打印,是打印无符号整形
    原码:111111111 111111111 111111111 10000000
    转换成十进制和例2的数字一样
    在这里插入图片描述

    小结:
    整形提升的时候,看的是你原先是char a还是unsigned char a
    但是打印的时候%d则视最高位为符号位,是否把补码转换成原码你得考虑正负。
    %u则视最高位为有效位,补码也就是原码,直接打印补码即可。

    例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

    计算机中加减操作都是对补码进行的
    -20的原反补如下:
    原:10000000 00000000 00000000 00010100
    反:111111111 111111111 111111111 11101011
    补:111111111 111111111 111111111 11101100

    10原反补相同
    补:00000000 00000000 00000000 00001010

    10+20补码
    111111111 111111111 111111111 11110110
    ps:这里进行计算时会进行算术转换,整形变成无符号整形
    总而言之,你是什么类型的数据存储在内存里不重要,我打印的时候再决定你最后是什么类型

    我们打印时,是以%d的形式进行打印,是打印有符号整形
    这里就要把补码转换成原码

    反码111111111 111111111 111111111 11110101
    原码10000000 00000000 00000000 00001010
    转换成10进制就是-10
    在这里插入图片描述

    例5:

    #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

    在这里插入图片描述
    解释如下图:
    在这里插入图片描述

    上图也可以画成一个圈来帮助记忆,如下图
    在这里插入图片描述
    知道了signed char 的存储,该题基本上就迎刃而解了。
    我们把数组a的内容画出来:
    在这里插入图片描述

    知道了数组a里面放的内容,还要知道strlen这个函数的特性,它是检测到\0停止,\0不计算
    而我们的0,存放在内存里也就是\0,所以第一次检测到0就结束了。

    0前面一共有-1 ~ -128还有127 ~ 1,一共是128+127=255个字符,所以打印255

    例6:

    #include
    unsigned char i = 0;
    int main()
    {
    	for (i = 0;i <= 255;i++)
    	{
    		printf("hello!\n");
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    解释如下:
    在这里插入图片描述
    unsigned char也是0000 0000 ~ 1111 1111,只不过这里最高位是有效位(你可以理解为全是非负数)

    最高的数也就是1111 1111,转换成10进制是255,然后我们这里1111 1111再+1会变成1 0000 0000
    但是unsigned char只有8位,会发生截断,又会变成0000 0000

    所以我们这里会变成死循环,永远无法大于255

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

    常见的浮点数:
    3.1415926
    1E10
    ps: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
    • 14
    • 15

    在这里插入图片描述
    对于第一个值9,因为我们&n得到了n的地址,
    然后强制类型转换变成了float类型的指针(和int类型指针一样,大小都是4字节),
    把这个指针赋给float*类型 pFloat,虽然指针类型变了,但是指针里面存储的地址还是n的地址,
    所以可以通过pFloat找到n,然后以%d形式打印n的值9

    而第一个*pFloat的值,我们知道pFloat是存储n的指针,
    然后 *pFloat就是对这个指针解引用,得到该地址里面的值,
    但是为啥打印的是0.000000呢?
    我们n是以整形的形式放进去的,但是我们要拿出来,也就是以%f形式打印,
    发现是0.000000,就说明浮点型的存储方式和整形不一样,如果一样,那拿出来应该也是9

    对于num的值,我们*pFloat = 9.0,也就是找到n的那块地址,然后把该地址上的值用浮点数9.0赋值,也就是说,我们是用浮点型的形式放进去了。然后我们%d打印该值,发现也不是9.0,
    再次验证浮点型的存储方式和整形不一样

    第二个*pFloat的值,因为我们是以浮点型的形式放进去,
    然后再以浮点型的形式拿出来(以%f的形式进行打印),所以我们这里可以正常打印9.0

    num 和 * pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
    要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。
    在这里插入图片描述
    在这里插入图片描述
    十进制5.5转换成二进制101.1后,再使用科学计数法,就是1.011*2^2

    所以我们计算机中的5.5表示如下
    (-1)^0 * 1.011 * 2^2
    S=0
    M=1.011
    E=2

    然后类比其他的浮点数,我们也是可以通过SME这三个数很快的还原出来
    那么我们内存里也不用存那么多数字了啊,只要存S、M、E即可
    ps:对于M和E实际上是存的是和它们相关的值,不是直接存M和E
    在这里插入图片描述
    在这里插入图片描述
    IEEE 754对有效数字M和指数E,还有一些特别规定。

    关于有效数字M
    前面说过, 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,即1000 1001。
    再比如,2^-1的E是-1,所以保存成32位浮点数时,必须保存成-1+127=126,即0111 1110

    在这里插入图片描述

    解释前面的题目:

    #include
    int main()
    {
    	float f = 5.5f;
    	//5.5转换成二进制是101.1
    	
    	//(-1)^0 * 1.011 * 2^2 
    	//S = 0
    	//M = 1.011
    	//E = 2
    
    	//E=2,根据IEEE754,存储在内存中是2+127=129,转换成二进制是1000 0001
    	//0 10000001 01100000000000000000000
    	//S E        M
    	
    	//上面的01000000101100000000000000000000转换成十进制就是1,085,276,160
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    五、总结

    本文介绍了整形和浮点型的数据类型,对于整形又细讲了原码、反码、补码和整形提升相关知识点,整形提升的例题也在文章着重讲解了。对于浮点型则是细讲了IEEE754规定的存储。本文相对而言并不复杂,相信耐心学习的你一定有所收获!

  • 相关阅读:
    【设计模式_实验①_第六题】设计模式——接口的实验&模拟应用实验作业一
    【Spring笔记05】Spring的自动装配
    R语言使用epiDisplay包的aggregate函数将数值变量基于因子变量拆分为不同的子集,计算每个子集的汇总统计信息、计算单个连续变量的分组汇总统计信息
    淘宝商品详情 API 返回值说明
    Unity json反序列化为 字典存储
    Cadence 快捷键
    Editor.md-编辑器
    豪赌?远见?浙江东方的量子冒险
    Docker24:compose下载安装步骤 + compose核心概念 +常用命令
    分布式锁的几种实现方式
  • 原文地址:https://blog.csdn.net/m0_57180439/article/details/126267139