都2023年了还有程序员搞不明白数据的存储?一篇文章带你全面学会数据存储

数据类型概念

基本的数据类型

  1. 整形家族:char 字符数据类型、short 短整型、int 整型、long 长整型、long long 更长的整型 (只有整型家族可以用 signed 和 unsigned 修饰)
    用short 还是int 还是long ,取决于对象的大小,比如:年龄-short 人口-long 地月距离-int
    整形家族的数据范围:
    char:
    unsigned char (0~255) %c %s
    signed char (-128~127) %c %s
    short:
    unsigned short [int] (0~(2^16-1)) %hu
    signed short [int] (-2^15~(2^15-1)) %hd
    int:
    unsigned int (0~(2^32-1)) %u
    signed int (-2^31~(2^31-1)) %d
    long:
    unsigned long (0~(2^32-1))
    signed long (-2^31~(2^31-1))

  2. 浮点型家族:float 单精度 %f、double 双精度 %lf (实型数据)

  3. 构造类型:int arr[10] 数组类型、struct 结构体类型、enum 枚举类型、union 联合体类型(自定义类型)
    int arr[10] 其组成为 数组类型int[10] 和 数组名arr,或解读为数组名是arr,数组中有10个元素,每个元素的类型是int

  4. 指针类型:int*char*double*

  5. 空类型:void
    常用于函数返回类型、函数参数、指针
    void test() //函数返回类型
    {}
    int test(void)//函数形参是void,表示函数不需要参数
    {}
    void* p;//无具体类型

不同的数据类型在内存中都是不同字节大小的空间,那么不同类型的意义就在于使用这个类型开辟内存空间的大小,以及如何看待内存空间的视角而决定。

整形数据在内存中的存储

数据在内存中都以二进制的形式存储(数据处理有三个板块,存储,取出,打印)
对于整数(此处谈的都是signed类型)来说:
整数二进制有三种表示形式:原码、反码、补码
对正整数来说:三种表示形式相同
对负整数来说:三种表示形式需要进行计算
如何计算?
举例: -10
原码:按照数据的数值直接写出的二进制序列就是原码
原码:10000000000000000000000000001010(总共三十二位,最高位为符号位,其用1表示负数,用0表示正数,剩下三十一位为数据位)
反码:在原码基础上,符号位不变,其他位按位取反,就是反码
反码:11111111111111111111111111110101
补码:反码 +1 ,就是补码
补码:11111111111111111111111111110110

整数在内存中存储的是补码
原因:
CPU上只有加法,没有减法
那么 1 - 1 的实际计算为 1 + (-1)——用二进制计算
在这里插入图片描述

结果为33位,但是最高位在int大小的内存中存不下,发生截断,剩下前32位,也就完成了计算,得到了结果 0
(ps:符号位也参与计算)

截断:如果将字节多的数据类型赋给一个占字节少的变量类型时或者高字节位的数据以低字节的数据类型进行打印的时候,都会发生“截断”,截取方式为,从低位截取出满足变量长度的位数。

在这里插入图片描述

如果用原码、反码,计算结果有误,用原码为例:
1 + (-1)
00000000000000000000000000000001
+
10000000000000000000000000000001
=
10000000000000000000000000000010(结果为-2,错误)

好处:补码与原码的相互转换,其运算过程是相同的,不需要额外的电路
(-1):
补码:11111111111111111111111111111111
取反:10000000000000000000000000000000(取反)
原码:10000000000000000000000000000001(+1)

以上分析总结:数值一律都用补码进行表示存储,CPU运算器也是用补码进行运算。(打印时用的原码打印) 原因在于,使用补码,可以将符号位和数值域统一处理(符号位参与计算);同时,加法和减法也统一处理(CPU只有加法器),此外补码和原码互相转换,其运算过程是相同的,不需要额外的硬件电路。
**

大端小端

大端字节序和小端字节序
int a = 0x11223344 在内存中有两种存储方式

  1. 大端字节序存储:
    在这里插入图片描述
    把数据的低位字节序(44)放在高地址处,把数据的高位字节序(11)放在低地址处。(大段反着来:低位高地址,高位低地址)
  2. 小端字节序存储
    在这里插入图片描述
    把数据的低位字节序(44)放在低地址处,把数据的高位字节序(11)放在高地址处。(小端正着来:低位低地址,高位高地址)

int a = 0x11223344; &a为0x0093FD6C,a为整型,用四个字节存储信息,也就是0x0093FD6C ~ 0x0093FD6F都是a的地址 在VS中0x0093FD6C中放的是44,0x0093FD6F中放的是11,也就是说VS的编译器是用小端字节序的方式进行数据存储

为什么数据要分大小端存储?
为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型, 32bit的int型(要看具体的编译器) , 另外,对于位数大于8位的处理器,如1 6位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。(官方)

每个地址对应一个字节,但是一个数据可能是short、int这类大于一个字节的类型,所以为了存储这些数据,那么会用到2个地址,4个地址,所以会涉及到如何安排这些地址的问题,也就出现了大端小端存储的模式。(个人理解)

此外,数据用大端(小端)存储,那么也会用大端(小端)解析,打印。

题目:写一个程序,判断当前机器的大小端存储类型

int main()
{
    int a = 1;
    char* pa =char*&a;//用char*去取出整型a中的首地址(最低的地址)
    if(*pa = 1)
    {
        printf("当前 机器是小端存储");
    }
    else
    {
        printf("当前机器是大端存储");
    }
}

隐式类型转换(整型提升、算数转换)

整型提升

表达式(包含操作符和操作数,操作符不是必要的)计算时,字符和短整型操作数在使用之前被转换为普通整型int,这种转换称为整型提升

整型提升的具体操作:
按照变量的数据类型(char、unsigned short、…)的符号位和具体数据来提升的,具体提升方式看举例。
ps:无符号类型所有位数都是数值位,原反补都一样,比如 -1 赋值给 unsigned char a ,a的内存中为11111111,printf输出结果为255
举例:
例一:

char c1 = -1;
//-1在内存中为111111111111111111111111 11111111,发生截断,c1在内存中存储的是11111111
 
char c2 = 1;
// 1在内存中为000000000000000000000000 00000001,发生截断,c2在内存中存储的是00000001
 
unsigned char c3 = -1;
//-1在内存中为111111111111111111111111 11111111,发生截断,c1在内存中存储的是11111111,最高位也是数值位——255

例二:

char a =3;
//00000000000000000000000000000011是3的二进制(整数3 默认为int型),但这里要存入字符型a中,发生截断,a中为00000011
 
char b = 127;
//同理b中截断后为,b中为01111111
 
char c = a + b;
//表达式中发生整型提升,计算出结果为00000000000000000000000010000010,又截断,c中存的是10000010(-2)
 
printf("%d",c);
//字符型变量c整型提升,11111111111111111111111110000010,打印是用原码打印,计算出原码为,10000000000000000000000001111110(-126),结果为-126

例三:

char a =0xb6;
short b = 0xb600;
int c = 0xb6000000;
if(a == 0xb6)
    printf("a");
if(b == 0xb600)
    printf("b");
if(c == 0xb6000000)
    printf("c");
//最终结果为c,因为变量a和b发生截断,分别不等于发生了整型提升的0xb6和0xb600

例四:

char a = 1;
printf("%u\n", sizeof(c)); //1,c本身的大小
printf("%u\n", sizeof(+c));//4,c参与了计算,也就变成了表达式,会发生整形提升
printf("%u\n", sizeof(-c));//4,同上
printf("%u\n", sizeof(!c));//4,同上,但是在VS中输出结果为1,这是错误的,在gcc下是4,gcc更符合C语言的语法规则
//例四补充:
int a = 3;
int b = 4;
a + b;//一个表达式有两个属性,值属性——算出一个7,类型属性,int型
short s = 0;
printf("%d\n", sizeof(s = a +3));//2  sizeof不会真正的运算,但编译器能够推算出计算后的类型,
//这里把两个整型放在了短整型s中,那么sizeof计算结果是短整数的字节数2,也能解释上面sizeof(+c)结果是4
printf("%hd\n", s); //0,印证sizeof内部不发生运算
`` 

整型提升的原因:
表达式的整型运算在CPU内相应的运算器中执行,CPU内整型运算器(ALU)的操作数的字节长度一般为int的长度,同时也是CPU通用寄存器的长度,因此,就算是两个char类型相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度,也就是int 或者 unsigned int,然后才能送去CPU内执行运算

补充:char到底是signed char 还是 unsigned char?
——C语言标准没有规定,取决于编译器(大部分都是signed char)
其他的数据类型比如int、short、long、double,在没有指明是signed还是unsigned时,C语言标准规定了是signed

算术转换

如果某个操作符的各个操作数属于不同的类型,那么只有其中一个操作数转换为另一个操作数类型,操作才能进行。
包括:
long double > double > float > unsigned long int > long int > unsigned int > int
算数转换是向上转换(向精度更高的类型转换),比如int类型的变量和float类型的变量做运算,那么int类型变量会算数转换成float类型。

显式类型转换(强制类型转换)

强制类型转换可能会改变变量访问的空间大小,也可能改变存储结构

改变访问空间大小:

char a = 10;
int b = (int)a;//此时a的空间大小从1个字节变成四个字节
int a = 0x11223344;
char b = (char)a;//机器为小端存储的话,发生截断,b变量会留下0x44

改变存储结构:

double a = 10.9;
int b = (int)a;//a的存储结构发生变化,直接向下取整得到10,存储在b中

对于指针变量也可能需要强制类型转换:

int a = 0;
int* pa = &a;
float* pb = (float*)pa;
//这里虽然pa和pb都存储的是地址,都访问四个字节,但是当解引用时,对它们各自指向的空间的解析方式不同,所以这里也需要强制类型转换

关于强制类型转换的特性 - 临时匿名变量

float a = 10.5;
int b = (int)a;
printf("%f\n", a);//10.500000
printf("%d\n", b);//10
//在a强制类型转换之后打印a,发现还是浮点数,说明a没有真正的发生变化
//原因:编译器会在内存的另一个地方创建一个临时变量x,该变量的类型为强制类型转换后的那个类型,这里就是int,
//然后将float类型的a变量的整数部分赋值给临时变量x,然后将x变量赋值给b变量,然后将临时变量x销毁,即完成此次强制类型转换
  • 整型数据存储相关例题:
char a = -128;
//-128原码:10000000000000000000000010000000
//    反码:11111111111111111111111101111111
//    补码:11111111111111111111111110000000
//放在a中:10000000
printf("%u\n",a);//4294967168
//%u是无符号十进制整型打印,所以 a 需要整型提升
//10000000 ——> 11111111111111111111111110000000(先按照原符号位进行整型提升,再考虑是以什么格式字符打印),这里是%u,所以不需要截断,直接看作无符号整型,转换成十进制即可

上例补充:
在这里插入图片描述在这里插入图片描述

关于格式字符:%c %s %d %u %hd …,不同的输入输出格式,可以理解为看待内存数据的视角,比如,内存中有一个四个字节的数,情况一:用%d,就将内存中这个数看作有符号的整型;情况二:用%c %hd,就会发生截断,打印截断后的数;情况三:用%u,就将内存中这个数看作无符号整数。
也就是说不管变量本身是什么类型,在打印时打印出什么数据,都是由格式字符来控制。

整型数据存储相关例题(继):

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

发现死循环,因为无符号数恒大于等于零,具体打印内容为9 8 7 6 5 4 3 2 1 0 4294967295(2^32-1) …3 2 1 0 4294967295 …

int main()
{
    char arr[1000];
    int i = 0;
    for(i = 0; i < 1000; i++)
    {
        arr[i] =  -1 - i;
    }
    printf("%d",strlen(arr));
    return 0;
}

结果为255,arr[0] = -1 arr[1] = -2 … arr[127] = -128 arr[128] = 127(原因:
-129,char类型变量存不下,具体情况是,-129 二进制为 1000… 00 10000001,求其补码 111…11 01111111,截断,存后八位也就是127) arr[129] = 126 … arr[254] = 1 arr[255] = 0(char类型的0也就是 ‘\0’ ) ,也就是说strlen在arr[255]时找到了’\0’,那么字符串长度为arr[0]~arr[254]也就是255个

**关于char类型(signed和unsigned都可以用)数据超出的快速口诀:
**超出范围的数据如果是正数,那么-256,如果超出范围的数据是负数,那么+256,比如上面-129超出了,那么就 -129 + 256 = 127(不管超出多少,都可以这么做)

unsigned char i = 0;
int main()
{
     for(i = 0; i<= 255; i++)
    {
         printf("haha\n");
     }
    return 0;
}

死循环,因为 i 是无符号整型,范围在0~255,每次超出范围都会发生截断(可用口诀计算具体值),使 i 恒小于255

整型家族数据类型的取值范围定义在limits.h这个头文件中
浮点数家族数据类型的取值范围定义在float.h这个头文件中

查找头文件可以在VS路径下/VC/Tools/MSVC/14.31/include中找到

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

引例

int main()
{
       int n = 9;
       //情况一
       float* pFloat = (float*)&n;  //变量n是int类型,用四个字节地址存放,这里用float* 类型去存储
       printf("n的值为:%d\n", n);//9,以整型的视角打印
       printf("*pFloat的值为:%f\n", *pFloat);//0.000000 对pFloat解引用,因为是float*类型的,可以访问到四个字节,这四个字节地址也就是储存变量n的四个字节的地址,不同的是,这里是用float*(以浮点数)的视角看看待这个四个字节里的内容,也就是用%f的方式打印。
       //情况二
       *pFloat = 9.0;*//以浮点数的视角存储9.0
       printf("num的值为:%d\n", n);//1091567616  以%d整型的视角打印
       printf("*pFloat的值为:%f\n", *pFloat);//9.000000,以%f浮点型的视角打印

       return 0;
}

通过两次打印结果的不同,说明整型和浮点型在内存中的存储(存进去)方式与解读(打印)方式都不同

一个float*的指针解引用访问4个字节,一个double*的指针解引用访问8个字节,一个char*的指针解引用访问1一个字节。

正式讲解

常见的浮点数:3.1415926 1E10(1.0×10^10)
浮点数家族包括:float、double、long double类型(C99标准引入了long double,很多编译器不支持)。

根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数 V 可以表示成下面的形式:

(-1)^S × M × 2^E

(-1)^S——表示符号位,当S = 0;V为正数;当S = 1,V为负数(与整数规则统一)
M ——表示有效数字,大于等于1,小于2 (二进制,类似于十进制的科学记数法中大于等于1小于10)
2^E——表示指数位(E的真实值范围为float为-127~128、double为-1023~1024)

举例:十进制浮点数:5.5
十进制5.5先转化成二进制 ——> 二进制:101.1(分两部分,小数点左边 ——5 = 2^2 + 2^0,得到101,小数点右边——0.5 = 2^(-1),得到1,组合起来就是101.1)
然后101.1用科学计数法表示,101.1 -> 1.011×2^2 -> (-1)^0×1.011×2^2 ,观察这个数,(-1)^0也就是标准形式中的(-1)^S,1.011也就是M,2^2也就是2^E
S = 0,M = 1.011 ,E = 2

可以理解为二进制的科学计数法
浮点数的十进制转二进制(补):
在这里插入图片描述

存入浮点数:
IEEE规定,对于32位的浮点数,最高位1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M,对于64位的浮点数,最高位1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
在这里插入图片描述
在这里插入图片描述

IEEE 754对有效数字M和指数E ,有一些特别规定。
前面说过 ,1≤ M < 2 , 也就是说, M可以写成1. xxxxx的形式,其中xxxxxx表示小数部分。IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1 , 因此可以被舍去,只保存后面的xxxxx部分。比如保存1.01的时候,只保存01 ,等到读取的时候,再把第一位的1加上去。 这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字(小数点左边的1位+小数点右边的23位)。

至于指数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。

int main()
{
    float f = 5.5f//数据结尾加一个f表示数据为float类型
    //上面得到了 1.011 * 2^2
    //s = 0,M = 011(011是M的计算值,真实值是1.011),E = 2 + 127(129是E的计算值,真实值是2)
    //0 10000001 01100000000000000000000(指数部分转换成二进制占满8位即可,有效数字部分照着写后,低位补0即可)
    //二进制:0100 0000 1011 0000 0000 0000 0000 0000
    //十六进制:4 0 b 0 0 0 0 0
}

在这里插入图片描述

打印浮点数:
E不为全0或不全为1(大多数情况)
将指数E的计算值减去127 (或1023) , 得到真实值,再将有效数字M前加上第一位的1。比如: 0.5的二进制形式为1.0×2^(-1) ,取E的计算值为126,减去-127得到E的真实值-1,取的M的计算值为0,则加上第一位的1,还原为1.0,并加上符号位的0/1,综上即可得到真实值1.0×2^(-1) 其阶码为-1+127=126 ,表示为01111110 ,而尾数1.0去掉整数部分为0 ,补齐0到23位000000000000000000000 ,则其二进制表示形式为:

E全为0
E全为0,只有当真实的E为-127时,加上127,才能让计算值E全为0,也就是表明,真实值非常小,无限接近于0。此时我们规定浮点数的E等于 -126(1 - 127)或者 -1022 (1 - 1023)即为真实值,有效数M不再加上第一位的1,而是直接在M计算值前填上0.即为0.(计算值),0.(计算值)× 2^-126,记得前面的正负号,这样就能直接表示出0以及非常接近于0的数。

E全为1
E全为1,只有当真实的E为128时,加上127,才能让计算值E全为1(十进制255),也就是表明,真实值非常大,1.xxxxx × 2^128,我们目前阶段,认为它是一个无穷大的数。基本不给予考虑。

浮点数打印只打印小数点后六位(不规定格式的情况下)

回顾引入部分的例子:

int main()
{
       int n = 9;
       //情况一
       float* pFloat = (float*)&n;  //变量n是int类型,用四个字节地址存放,这里用float* 类型去存储
       printf("n的值为:%d\n", n);//9,以整型的视角打印
       printf("*pFloat的值为:%f\n", *pFloat);//0.000000 对pFloat解引用,因为是float*类型的,可以访问到四个字节,这四个字节地址也就是储存变量n的四个字节的地址,不同的是,这里是用float*(以浮点数)的视角看看待这个四个字节里的内容,也就是用%f的方式打印。
       //情况二
       *pFloat = 9.0;*//以浮点数的视角存储9.0
       printf("num的值为:%d\n", n);//1091567616  以%d整型的视角打印
       printf("*pFloat的值为:%f\n", *pFloat);//9.000000,以%f浮点型的视角打印

       return 0;
}

第二个printfprintf("*pFloat的值为:%f\n", *pFloat);,9转换成二进制为
00000000 00000000 00000000 00001001,然后我们以浮点数的视角去看待,那么解读这四个字节空间的方式为
0 00000000 00000000000000000001001,s = 0 ,E = 0, M = 00000000000000000001001 ,真实值是0.00000000000000000001001×2^(-126),但是我们实际打印时,只打印前6位小数,即0.000000

第三个printfprintf("num的值为:%d\n", n);,9.0作为浮点数储存在内存中为0 10000010 00100000000000000000000,但是这里打印是以整型视角进行打印,符号位是0,说明是正数,原反补相同,直接将01000001 00010000 00000000 00000000,转换成十进制即是答案

此外:

float f = 9.0;
printf("%d\n",f);//0,这样还是以浮点数的形式拿出来了,却用%d的方式打印,不妥

float f = 9.0;
int* p = (int*)&f;//这样我们用强制类型转换,将内存中这段数据以整型的方式解读
printf("%d\n",*p);//1091567616,*p——也就是用整型的视角看待数据,拿出数据,以整型的方式进行打印

小结

我们学习数据的存储是修炼内功,能让我们对内存有着更深刻的认识,可能平时不会用到太多这方面的知识,但是一旦当存储、打印时出现问题,这些知识会排上大用处,也能在出现一些奇奇怪怪的结果时不慌张,希望读者能有所收获。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

失去梦想的小草

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值