算术移位和逻辑移位详解

大部分的C编译器,用移位的方法得到代码比调用乘除法子程序生成的代码效率高。

移位运算是将数值向左向右移动,对于十进制来说就是实现放大十倍和缩小十倍的效果,而对于二进制而言就是放大两倍和缩小两倍的效果

整数的乘除法

一个自己曾经忽视的东西,那就是C/C++中的移位操作容易出错的情况。

1、什么样的数据类型可以直接移位

char、short、int、long、unsigned char、unsigned short、unsigned int、unsigned long都可以进行移位操作,而double、float、bool、long double则不可以进行移位操作。

2、有符号数据类型的移位操作

对于char、short、int、long这些有符号的数据类型:

对负数进行左移:符号位始终为1,其他位左移

对正数进行左移:所有位左移,即 <<,可能会变成负数

对负数进行右移:取绝对值,然后右移,再取相反数

对正数进行右移:所有位右移,即 >>

这里很重要,具体为啥是这样,文章之后会解释!

3、无符号数据类型的移位操作

对于unsigned char、unsigned short、unsigned int、unsigned long这些无符号数据类型:

没有特殊要说明的,使用<< 和 >> 操作符就OK了

算术移位和逻辑移位运算

  1. 逻辑移位

    对于逻辑移位,就是不考虑符号位,移位的结果只是数据所有的位数进行移位。根据移位操作的目的,左移时,低位补0,右移时,高位补0;

    例:

    01010101>>3=00001010

    01101011<<3=01011000

    Q1:此时是不是就有一个问题,不考虑符号位,如果一个负数,逻辑右移,结果就会变成正数,例如

    10000101=-5>>1=01000010=66

    的确是的,因为这是逻辑运算的特点,算术移位运算才会考虑符号位!

    Q2:如果一个有符号数,逻辑左移,导致符号位变化,此时也算逻辑运算的特点吗?

    例如:10000001=-1<<1=00000010=2;

    这里只是溢出,并不算特点,类型溢出在算术移位中也存在!

    另外,C语言中没有无符号移位运算符,怎样才能实现无符号右移呢?负数进行无符号右移,很明显,符号位被0填充,变成正数,这种没有意义,一般都是正数进行无符号位移位,可以强制转换,可以使用除法!

    更严谨的写法:

    1.先进行类型转换
    ((unsigned int)x);

    2.将要移动的数字和0xFFFFFFFF进行与运算,就可以转换为unsigned 类型了
    (0xFFFFFFFF&x);

    这里假设int型是32位的…

  2. 算术移位

    算术是带有符号的数据,所以我们不能直接移动所有的位数,这可能会使得符号不正确。

    一个很重要的知识:

    关于数的移位,特别需要注意正数,三码相同,所以无论左移还是右移都是补0.而负数的补码就需要注意,左移在右边补0,右移需要在左边补1,有一个很有趣的误区是,认为符号位保持不变,仅仅移动数据位,这是不对的,因为无论数据位还是符号位,都是二进制,在整体大迁移的过程中,符号位也是要跟随潮流的。只不过,为了保证右移后,和原来的符号数一样,因此,负数在右移时左边补1.

    比如8位机器数(补码):1,110 0110,右移一位是:1,111 0011,开始的符号位变成了现在的数据位。

    (1). 原码
    原码就是多了一个符号位,所以符号位不变,其余数值位当做***逻辑移位***来处理即可。

    这里为什么说当逻辑移位运算即可呢?

    因为计算机中存储数据,都是用补码存储的,整数的补码,反码,原码都相同,因此对原码进行算术移位运算,都是对正数进行算术运算,因此用逻辑移位来处理即可!

    (2). 反码
    类似原码,符号位不变,其余数值位当做逻辑移位来处理,但是对于负数,补0的时候应该补1,这是因为负数的反码“0”的效果和正数的“1”产生的效果是一样的。

    (3). 补码
    观察补码表,不难知道,当左移移出的数据位正数为“0”、负数为“1”时(只有这时候该数值小于等于最大值/2),一定不发生溢出。因为补码是计算机储存的形式,所以在硬件实现的时候为了方便和简化,左移:假设不发生溢出,直接将数据最高有效位移入符号位,最低位补0(为什么负数最低位不是补1?如果是反码补1没错,但是补码是反码加了1,所以最低位还是补0)。右移:假设不发生溢出,符号位不变,同时用符号位补数值最高位。

    比如,将一个有符号数左移移位,C++编译器的运行会是怎样的呢?

    #include 
    #include 
    using namespace std;
    
    int main()
    {
        char a=-65;
        a=a>>1;
        printf("%d",a);
        return 0;
    }
    

    查看main函数汇编代码:

    main:
     push   rbp
     mov    rbp,rsp
     sub    rsp,0x10
     mov    BYTE PTR [rbp-0x1],0xbf
     movsx  eax,BYTE PTR [rbp-0x1]
     sar    eax,1
     mov    BYTE PTR [rbp-0x1],al
     movsx  eax,BYTE PTR [rbp-0x1]
     mov    esi,eax
     mov    edi,0x4007d5
     mov    eax,0x0
     call   4005a0 
     mov    eax,0x0
     leave  
     ret  
    

    这里稍微补充一下汇编知识点,比如说MOV BYTE PTR[BX] 10H
    源操作数用的是立即寻址,相当于直接赋值10H,目的操作数用的是寄存器间接寻址,假设BX里的内容(BX)=1000H,(DS)=2000H,那么对应的物理地址为 DS X 10H+BX =21000H,现在这个物理地址对应的单元内容是10H

    mov BYTE PTR [rbp-0x1],0xbf,这句汇编,意思就是将0xbf这个立即数放到(DS) X 10H+rbp-0x1的物理地址中;对应C++中的char a=-65;

    现在来好好看看0xbf,我们知道,-65=11000001,转换为补码,就是除符号位外,其他取反加一,补码为10111111=0xbf !!!

    也就是说,C++在存储数字的时候都采用补码的形式;

    将10111111>>1,结果为11011111,转为原码,也是除符号位外,其他取反加一,为10100001=-33;

    的确,编译器的结果就是-33

    Program returned: 0
    
    Program stdout
    
    -33
    

    如果-65>>2,结果会是多少呢!

    11101111->10010001=-17

    这里为什么会是-17呢!

    10111111>>2,就变为,101111,符号位依然是1,那第七位是补1还是补0呢!

    上面补码中有描述,右移:假设不发生溢出,符号位不变,同时用符号位补数值最高位。

    也就是第七位补1,即移位后变为11101111,原码位10010001=-17;

    如果-65>>3,移位后,10111,符号位为1,第七位补1,那第六位呢!其实,算术右移,左侧均补符号位,即正数补0,负数补1。

    -65>>3=11110111,原码为,10001001=-9

知道了编译器是通过补码形式进行算术移位运算,考虑了算术右移的情况,那么来考虑算术左移的情况,

左移里一个比较特殊的情况是当左移的位数超过该数值类型的最大位数时,编译器会用左移的位数去模类型的 最大位数,然后按余数进行移位,先不考虑这种情况!

考虑正常的情况,左移的规律如下!

左移:假设不发生溢出,直接将数据最高有效位移入符号位,最低位补0

左移的描述中有一个假设词,即假设不发生溢出,如何保证不溢出呢?

计算机是这样做的,为保证补码算术左移时不发生溢出,移位的数据最高有效位必须与符号位相同。什么叫 移位的数据最高有效位,即移位后,成为最高符号位的数据位。在不发生溢出的前提下,用硬件实现补码的 算术左移时,直接将数据最高有效位移入符号位,不会改变机器数的符号。

移位的数据最高有效位必须与符号位相同?和后面这句话是什么意思啊~?

取一字节为例,能表示的数字从 -128 ~ 127

先看正数,比如 9,就是 0000 1001,最高位(符号位)和数据最高位都是 0. 左移得到 0001 0010 也就是 18,再看负数 -9,就是 -(0000 1001) 也就是 1111 0110 + 1 即 1111 0111,最高位(符号位)和数据最高位 都是 1. 左移后是 1110 1110,反过来是 (1110 1101 + 1)->0001 0010,就是 -18 了。

如果数据最高位和符号位不同的话,说明这个数字的绝对值已经超过 64 了,那么左移一位后必然溢出。
所以一个【有效的】左移最高位和数据最高位必然一致。故算术左移和逻辑左移一样。

  1. 算术移位和逻辑移位的比较

    根据上述的描述,可以归纳出(均为补码操作):

    1. 当一个【有效的】左移最高位和数据最高位一致时,算术左移和逻辑左移一样,均为右补0,不一致时,算术左移溢出,无意义!

    2. 逻辑右移很简单,只要将二进制数整体右移,左边补0即可 ,算术右移,左侧均补符号位,即正数补0,负数补1。

      不清楚的可以回头看看上文解析!

其中逻辑移位最简单,不管左移右移,移出来的空位补0即可。

算术移位特别需要注意,原来的符号位一样移动。因为移位是宏观的变化,不允许任何元素保持不动。所以左移时,正数有可能变为负数,负数有可能变为正数。因为左移原来的符号位丢了,右边补的是0。而右移时不会改变符号性,因为右移是将数据减半,减半不可能减成相反的符号的。而左移可能溢出,溢出的特征就是符号跃迁。

拓展知识

java移位运算符:<<(左移)、>>(带符号右移)和>>>(无符号右移)

  1. <<(左移)

    左移的规则只记住一点:丢弃最高位,0补最低位(此时也会溢出,但是我们也应该尽量避免这种溢出)

    如果移动的位数超过了该类型的最大位数,那么编译器会对移动的位数取模。如对int型移动33位,实际上只移动了332=1位。

  2. >>(带符号右移)

    右移的规则只记住一点:符号位不变,左边补上符号位

    按二进制形式把所有的数字向右移动对应的位数,低位移出(舍弃),高位的空位补符号位,即正数补零,负数补1

  3. >>>(无符号右移)

    无符号右移的规则只记住一点:忽略了符号位扩展,0补最高位

    无符号右移规则和右移运算是一样的,只是填充时不管左边的数字是正是负都用0来填充,无符号右移运算只针对负数计算,因为对于正数来说这种运算没有意义
    无符号右移运算符>>> 只是对32位和64位的值有意义

你可能感兴趣的:(java学习,C++,汇编基础)