查看原文
其他

【重磅】“整形数”还真没那么简单(C语言版)

bug菌 最后一个bug 2021-01-31

1、简单聊一聊

    今天为大家推荐一首周深的《大鱼》,真的是上天赐予的嗓子,与这首歌曲真的绝配!如果上天再给我一次选择的机会,我还是选择当一名程序员。
    好了,之前为大家写了一篇<【典藏】别怪"浮点数"太坑(C语言版本)>,很多小伙伴反馈到希望作者也补充一些整形数相关的坑。仔细想想平时我们用整形数比浮点数多得多,而且整形数的细节还真不少,为了便于以后大家编码等,这里作者根据自己的一些经验记录一下。

1、整形的存储

1)说说数据的存储 

    数据对于存储器而言都是二进制的0101...,也就是我们所说的机器码。而我们所定义的类型就相当于确定了这块内存占据多大的位置和以怎么这样的方式进行解析。比如说16进制:0xFF,在unsigned char中表示255,而在signed char中表示-1,其他数据类型也是类似的道理。如果你再看得抽象一点把指针拿过来一起理解,可以把变量看成是地址固定不变的指针。

2)聊聊原码、反码、补码 

    先上理论知识:(注意:浮点数不是以该方式存储)

原码:
  • 正数:符号位为0,其他为正常二进制。

  • 负数:符号位为1,其他为绝对值二进制。

反码:
  • 正数:与原码一致。

  • 负数:符号位为1,其他位按位取反。

补码:
  • 正数:与原码一致。

  • 负数:反码+1。

    对于整形数计算机上的存储都是以补码的形式进行存储,那么对于正数就按照正常的二进制进行存储,而负数则对原码进行反码+1存储,在我们平时进行仿真或者调试过程中用无符号输出负数会有一个非常大的数据,其实这个数据就是该负数的补码。

    比如:char类型的-3,其原码为1000 0011-->反码为11111100-->其补码为:11111101,如果我们采用unsigned char类型显示则为:253,如果我们知道原码那就反过来进行计算即可。同样其他整形数据类型也是一样的实现方式。

2、整形溢出问题

1)为什么用补码存储? 

    大家应该都知道1 - 1 = 1 + (-1) = 0,那么计算机为了简化运算就把(-1)用另外一种方式存储,这样计算机就只需要进行加法运算,于是便产生了补码。同样四则运算中的乘法和除法运算都可以通过加法进行表示。(有一种"道生一、一生二、二生三、三生万物"的精妙)

    通过上面我们也可以看出直接用原码进行计算,最终结果竟然成为了-2,明显不符合;而采用补码计算,由于采用byte计算,进位被截断了,从而获得了最终的结果0.同时使用补码的形式也规避掉了原码中0映射问题,如下图所示。

2)数据溢出问题 

    这里我以char和unsigned char类型来进行说明,对于其他整形数据同样分析即可,首选我们来看看使用补码以后的数据表示范围问题,目前最经典的图形表示方法就是采用环形表示,如下图:

    这样表示的好处是,一旦数据溢出,直接顺着变化的方向即可找到对应的值。这里也贴出实验的代码如下:

1#include <stdio.h>
2#include <stdlib.h>
3/********************************************************
4 * Fuction:测量char类型数据溢出问题 
5 * Author :(公众号:最后一个bug) 
6 *******************************************************/

7int main(int argc, char *argv[]) {
8    char Val           = 5;
9    char Val1          = 123;
10    unsigned char Val2 = 5;
11    unsigned char Val3 = 123;
12    int i = 0;
13
14    printf("char    |     char    |     uchar   |     uchar\n");
15    printf("-----------------------------------------------\n");        
16    for(i = 0;i < 9;i++)
17    {
18        printf("%4d  ******  ",--Val);  
19        printf("%4d  ******  ",++Val1);
20        printf("%4d  ******  ",--Val2);
21        printf("%4d\n",++Val3);
22    }
23    printf("\n公众号:最后一个bug\n"); 
24}

    最终输出的结果与我们的环形结构是相符合的,结果如下:

3)它来了!!! 

    提个几个问,有符号char类型中的-128取相反数会获得什么值?无符号取相反数又等于什么呢?不防敲个代码实验下,代码简单直接上结果:

    我们可以得出结论:相反数直接关于环形对称。同样其他的数据类型也是同样的性质,仅仅只是数据范围变大了。


3、算数转化

1)算数转化(前方高能)  

    首先我们来看一下一段简单的代码:(前方高能!!)

1#include <stdio.h>
2#include <stdlib.h>
3/********************************************************
4 * Fuction: 算数转化测试
5 * Author :(公众号:最后一个bug) 
6 *******************************************************/

7int main(int argc, char *argv[]) {
8    int Val1 = -2;
9    unsigned int  Val2 = 1;
10
11    if(Val1 > Val2)
12    {
13        printf("-2 > 1\n");     
14    }
15    else
16    {
17        printf("-2 < 1\n");     
18    }
19    printf("\n公众号:最后一个bug\n"); 
20}

    了解算数转化概念的小伙伴应该都知道,该程序并不会输出我们常规的-2 < 1,而是输出-2 > 1这个结果,这个与我们的常规结果有点不符合。(眼见为实,下图看结果)

2)汇编来坐镇  

    作者第一个接触到这个问题的时候都怀疑人生了,这C也太坑了,一言不合就把我给弄得团团转。既然C这样做肯定有其原因吧,分析疑难杂症从汇编做起:(DevC++,gcc-32bit)

    我们看到if语句对应的汇编cmp指令和jbe跳转指令;其中jbe用于判断无符号跳转指令,那么会把EAX直接当成无符号类型进行处理,从而得到了我们上述的结果,如果你把上面的代码Val2改成int类型,然后查看汇编文件会得到如下结果:

    其中唯一的区别就是jle,该汇编指令为有符号条件转移指令,你可以编译一下能够得到我们想要的结果。

3)算数转化总结 

    我们这里所说的算数转化其实就是一种隐式的强制类型转化,我们平时大部分都是使用的显示强制类型转化,就像我们上面的程序,其实这种情况是比较危险的,我们大部分汇编指令都是具有相同类型操作数,那么如果操作数类型不同,系统会根据数据类型的优先级进行自动转化。(大家可以参考下面的类型进行对应处理)

    原则:数据都是优先转化为长数据类型,浮点与整形优先转化为浮点运算,无符号与有符号优先转化为无符号。


4、整形数据的提升

    经常有很多小伙伴把整形提升与算数转化混合一起谈,其实算数转化是为了让操作数一致而进行的隐式类型转化,而整形提升是对于短类型转化为长类型进行处理的一种方式,这个是必然的过程,不管类型是否一致。

1#include <stdio.h>
2#include <stdlib.h>
3/********************************************************
4 * Fuction:整形提升 
5 * Author :(公众号:最后一个bug) 
6 *******************************************************/

7int main(int argc, char *argv[]) {
8    char          Val1 = 1;
9    unsigned char Val2 = 2;
10
11    printf("sizeof(Val1)        = %d\n",sizeof(Val1)); 
12    printf("sizeof(-Val2)       = %d\n",sizeof(-Val1)); 
13    printf("sizeof(Val2 - Val1) = %d\n",sizeof(Val2 - Val1)); 
14    printf("\n公众号:最后一个bug\n"); 
15}

    最终输出的结果:

解析一下:

    对于整形提升,其实主要是为了增加CPU运算效率,就跟我们前面说补码一样,CPU只想用加法就能够实现4则运算,那么其处理数据也是一样的,大部分的寄存器都是32位的(仅仅对于32位机器),比如上面汇编中的EAX寄存器,CPU就想直接处理32位的数,并且效率也高,计算完毕以后再转化为对应的类型获得最后的结果,对于char等等这些短数据类型在进行运算或者比较的过程中都会采用int类型进行处理,如果有更加长的数据类型会优先转化为更长的数据类型。


5、最后小结

    估计大家看完以后都不敢随便写代码了,其实不要慌,在编码的过程中一定要对每个变量的范围和变化都要了然如胸,在进行运算操作的时候最好是相同类型,切记最好不要将有符号和无符合混合使用,如果硬要混合记得显式强制类型转换。

    好了,这里是公众号:“最后一个bug”,一个为大家打造的技术知识提升基地。同时非常感谢各位小伙伴的支持,我们下期精彩见!

推荐好文  点击蓝色字体即可跳转

【典藏】别怪"浮点数"太坑(C语言版本)

 【经典】解析一个STM32在线升级实例(usart版本)

【典藏】深度剖析单片机程序的运行(C程序版) 

【重磅】剖析MCU的IAP升级软件设计(设计思路篇)

☞ 【解惑】到底是"时间片"?还是"分时轮询"?

GUI必备知识之“告别”乱码(浅显易懂)

【典藏】大佬们都在用的结构体进阶小技巧

听说因为代码没"对齐"程序就奔了?(深度剖析)

【典藏】自制小型GUI界面框架(设计思想篇)

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存