当前位置:网站首页 > 技术博客 > 正文

左移运算符溢出



        C语言是一种很奇妙的语言,它既有高级语言的特点,又有低级语言的特点,支持位运算让它更方便于硬件编程。

一、左移运算符(<<)

        左移运算就是将一个二进制位的操作数按指定位数整体向左移位,移出位被丢弃(是否丢弃也不一定,得看接收结果的数据类型范围),右边的空位一律补0。

语法:x << n,其中 x 是要移动的数字,n 是要移动的位数。

关联的数学公式:位左移结果 = 要移动的数字 * 2的n次方(n 是要移动的位数)。该公式不是总有效啊!

1、正数左移举例:

完整代码在后面。

这里我们用char类型,因为它范围是1个字节,8bit位,方便查看结果。

        char ch = 10;                 // 00001010
        // 左移1位
        char r1 = ch << 1;         // 00010100
        printf("10 << 1 = %d ", r1);    // 20 (10*2的1次方)


结果如下:

        // 左移2位
        char r2 = ch << 2; // 00
        printf("10 << 2 = %d ", r2);    // 40 (10*2的2次方)
        // 左移3位
        char r3 = ch << 3; // 0
        printf("10 << 3 = %d ", r3);    // 80 (10*2的3次方)




==== 下面是重点 ====

左移4位-->关键地方到了,因为左移4位后最左边是1了,也就是说符号位为1,可能变负数了。
        char r4 = ch << 4; // ,转换为十进制为 -32,但真的是-32吗?
        printf("10 << 4 = %d ", r4);

结果如下:

        答案是-96,正数左移真的有可能变成负数了,但为什么不是-32呢。
        因为数值在内存中存储的是补码,正数的原码、反码、补码是一样的。负数的补码=原码取反+1。
        我们对内存中的数左移时实际上都是对补码进行左移,实际取值时还要转换为原码的。
        ,符号位是1,负数,取原码时要先减去1,再取反才行。
            
          -                1    
          -----------------
               
     取反   -> -96
        原码、反码、补码不懂的可以看我以前写的文章。
        10 << 4 一定是负数吗?看下面代码
        short r44 = ch << 4;
        printf("10 << 4 = %d ", r44);    // 160 (10*2的4次方)











怎么样,没变负数吧,也就是说左边移动的4位并没有被舍弃,这是因为我们用short类型来接收的,该类型为2个字节,你左移8位都能完整接收,更何况仅仅是左移4位了,因此不会舍弃。

完整代码如下:

 
  
2、负数左移举例:

        注意:负数在内存中存储的是补码,对补码进行左移,再换算成原码才是我们要的结果。挺闹心的啊!但没办法,这不是我们能决定的。

        char ch = -10; // 原码 --> 补码 (负数要看补码啊!!!!!!!!!)
        // 左移1位
        char r1 = ch << 1;    // 补码 --> 原码
        printf("-10 << 1 = %d ", r1);    // -20 (-10*2的1次方)


结果如下:

        // 左移2位
        char r2 = ch << 2;    // 补码 --> 原码
        printf("-10 << 2 = %d ", r2);    // -40 (-10*2的2次方)
        // 左移3位
        char r3 = ch << 3;    // 补码 --> 原码
        printf("-10 << 3 = %d ", r3);    // -80 (-10*2的3次方)




结果如下:

======== 重点又到了 ========

        左移4位-->关键地方又到了,因为左移4位后最左边是0了,也就是说符号位为0,可能变正数了。
        char r4 = ch << 4;    // 补码 0 --> 原码 0
        printf("-10 << 4 = %d ", r4);    // 96 (数学公式不好用了)

结果如下:

        -10 << 4 一定是正数吗?看下面代码
        short r44 = ch << 4;
        printf("-10 << 4 = %d ", r44);    // -160 (-10*2的4次方)

结果如下:


        怎么样,没变正数吧,也就是说左边移动的4位并没有被舍弃,这是因为我们用short类型来接收的,该类型为2个字节,你左移8位都能完整接收,因此不会舍弃。

完整代码如下:

 
  

    总结:左移运算时正数可能会变负数,负数也可能会变正数,左移的位数也未必舍弃,就看你用什么类型接收它,这个要注意啊。

二、右移运算符(>>)

        右移运算就是将一个二进制位的操作数按指定位数整体向右移位,移出位被丢弃,左边的空位补符号位(还有一种情况是无论正负数都补0,这种情况不考虑,我们基本用不到)。
    语法:x >> n,其中 x 是要移动的数字,n 是要移动的位数。
    关联的数学公式:位右移结果 = 要移动的数字 / 2的n次方(n 是要移动的位数)。

    1、正数右移举例:

        // 这里我们用char类型,因为它范围是1个字节,8bit位,方便查看结果。
        char ch = 10; // 00001010
        // 右移1位
        char r1 = ch >> 1; // 00000101
        printf("10 >> 1 = %d ", r1);    // 5 (10/2的1次方)
        // 右移2位
        char r2 = ch >> 2; // 00000010
        printf("10 >> 2 = %d ", r2);    // 2 (10/2的2次方,这里小数丢失精度了)
        // 右移3位
        char r3 = ch >> 3; // 00000001
        printf("10 >> 3 = %d ", r3);    // 1 (10/2的3次方)
        // 右移4位
        char r4 = ch >> 4; // 00000000
        printf("10 >> 4 = %d ", r4);    // 0
        // 哪怕是我们用2个字节的short来接收结果也是一样的。
        // 右移4位
        short r44 = ch >> 4; // 00000000
        printf("10 >> 4 = %d ", r44);    // 0
















结果如下:

完整代码

 
  
2、负数右移举例:

        // 注意:负数在内存中存储的是补码,对补码进行右移,再换算成原码才是我们要的结果。挺闹心的啊!但没办法,这不是我们能决定的。
        char ch = -10; // 原码 --> 补码 (负数要看补码啊!!!!!!!!!)
        // 右移1位
        char r1 = ch >> 1;    // 补码 --> 原码
        printf("-10 >> 1 = %d ", r1);    // -5 (-10/2的1次方)
        // 右移2位
        char r2 = ch >> 2;    // 补码 --> 原码
        printf("-10 >> 2 = %d ", r2);    // -3 (-10/2的2次方)
        // 右移3位
        char r3 = ch >> 3;    // 补码 --> 原码
        printf("-10 >> 3 = %d ", r3);    // -2 (-10/2的3次方)
        // 右移4位
        char r4 = ch >> 4;    // 补码 --> 原码
        printf("-10 >> 4 = %d ", r4);    // -1 (-10/2的4次方)
        // 右移4位(强转),不论正负数,右移时前面一律补0
        char r44 = (unsigned char)ch >> 4;    // 补码 =>转化为无符号类型,再右移4位,由于是无符号类型,前面补0 => 00001111
        printf("(unsigned char)-10 >> 4 = %d ", r44);         // 15
        负数无论右移多少次,-1也就是到头了啊!
        short a = -1207;
        printf("-1207 >> 100 = %d ", -1207 >> 100);    // -1


















结果如下:

完整代码:

 
  

三、与运算符(&)

在编程中我们经常用到与运算符(&&),比如下面的代码:
if (1==1 && 2==2)
{
        printf("这里用到与运算符,二者都为真才是真,否则为假! ");
}
这个与运算符(&&)是针对字节而言的,要是针对bit位而言与运算符就是(&)了。
与(&)是用来比较2个bit位的,二进制码只有0与1,我们认为0是假,1是真。只有全是真才是真,否则都为假。因此我们得到以下结论:
    0 & 0 = 0;
    0 & 1 = 0;
    1 & 0 = 0;
    1 & 1 = 1;
    举个简单例子:
    10 & 3 = ?











       00001010 -> 10
  &   00000011 -> 3
    --------------------
       00000010 -> 2
    
    代码如下:




 
  

结果如下:


那么与(&)到底有什么用呢?
我们可以把二进制中的0与1当作电灯的开关(0-关灯,1-开灯),1个字节中的8bit当作8个开关,与(&)可以控制指定位置的开关为0,即关灯,其他位置不变。这点在电路上非常有用。
    认识关羽吗?想要灯(设置该bit位为0)吗?请用(与&),关二爷保佑你!
    比如说:
    二进制 0 1 0 0 1 1 0 1  -> 77(4D)
    我想让其第1、4、5位置为0(从右往左数),其它位置不变,只要让77(4D)和数-26(E6)【该数的1、4、5为0,其他位置位1】做与运算即可。





          0 1 0 0 1 1 0 1  -> 77   -> 4D
      &  1 1 1 0 0 1 1 0  -> -26  -> E6   【,这里用一个字节来演示,最高位为1,则该数是负数,是补码,需要转变为原码才行。但如果我们用16进制E6来表示该数则不需要考虑负数的事,因此很多代码都是用16进制来进行与运算的。】
      -------------------------------------------
          0 1 0 0 0 1 0 0  -> 68   -> 44


 
  

结果如下:

任何一个数与0做与(&)运算,结果一定是0。

四、或运算符(|)

    在编程中我们经常用到或运算符(||),比如下面的代码:
    if(1==1 || 2==3)
    {
        printf("这里用到或运算符,二者只要有一个为真结果就是真,否则为假! ");
    }
    这个或运算符(||)是针对字节而言的,要是针对bit位而言或运算符就是(|)了。
    或(|)是用来比较2个bit位的,二进制码只有0或1,我们认为0是假,1是真。二者只要有一个为真结果就是真,否则为假!因此我们得到以下结论:
    0 | 0 = 0;
    0 | 1 = 1;
    1 | 0 = 1;
    1 | 1 = 1;
    举个简单例子:
    10 | 3 = ?











       00001010 -> 10
    |  00000011 -> 3
    --------------------
       00001011 -> 11


 
  

    结果如下:

    那么或(|)到底有什么用呢?
    我们可以把二进制中的0或1当作电灯的开关(0-关灯,1-开灯),1个字节中的8bit当作8个开关,或(|)可以控制指定位置的开关为1,即开灯,其他位置不变。这点在电路上非常有用。
    比如说:
    二进制 0 1 0 0 1 1 0 1  -> 77(4D)
    我想让其第2、6、8位置为1(从右往左数),其它位置不变,只要让77(4D)和数-94(A2)【该数的2、6、8为1,其他位置位0】做或运算即可。



         0 1 0 0 1 1 0 1  -> 77   -> 4D
      |  1 0 1 0 0 0 1 0  -> -94  -> A2   【,这里用一个字节来演示,最高位为1,则该数是负数,是补码,需要转变为原码才行。但如果我们用16进制来A2表示该数则不需要考虑负数的事,因此很多代码都是用16进制来进行或运算的。】
      ----------------------------------
         1 1 1 0 1 1 1 1  -> -17   -> EF  【是负数的补码,转换为原码是-17】


    代码如下:

 
  

结果如下:

这里特意将符号位数字改为1,就是为了能练习一下负数的处理方式,否则用unsigned char变量会看起来更简单明了一些。

任何一个数与0做或(|)运算,结果一定是那个数。

五、异或运算符(^)

        异或运算就是将二进制的两个操作数的每一位进行比较,如果相同结果为0,如果不同结果为1。它主要是看2个操作数是否有差异。就好像我们常玩的消消乐游戏,把一样的图案消掉,结果为0,不一样保留,结果就为1。
    我们可以得出下面的结论:
    0 ^ 0 = 0;        // 一样,消消乐了
    0 ^ 1 = 1;        // 不一样,消不掉
    1 ^ 0 = 1;        // 不一样,消不掉
    1 ^ 1 = 0;        // 一样,消消乐了
    举个简单例子:
    10 ^ 3 = ?






       00001010 -> 10
    ^  00000011 -> 3
    --------------------
       00001001 -> 9
 



 
  

结果如下:

    就是这么简单。
    异或运算有一些很有意思的规律:
    1) a ^ a = 0;    // 自身异或,哪有差异性啊
    2) a ^ 0 = a;
    3) a ^ b ^ c = a ^ c ^ b = a ^ (b ^ c);        // 乘法交换律与结合律啊
    这里我们得出 a ^ b ^ a = a ^ a ^ b = 0 ^ b = b,该规律可以用于加密算法中。




举一些关于异或的小例子:

    1、可以使用异或来交换两个变量的值(不允许创建第3个临时变量来帮忙)。

 
  

结果如下:

        解析一下上面的代码,这里用a1,b1代表新的值;a,b代表原始值。
        a = a ^ b; // 这里左边的a值已经改变了,我们称之为a1,b值未做改变。即 a1 = a ^ b;
        b = a ^ b; // 这里代码相当于 b = a1 ^ b = a ^ b ^ b = a; b的值也改变了,我们称之为b1,此时存储的是a的原始值10,即 b1 = a。
        a = a ^ b; // 这里代码相当于 a = a1 ^ b1 = a ^ b ^ a = b; a此时存储的是b的原始值20.
        有点绕吧,哈哈!



2、利用异或加解密

 
  

结果如下:

六、取反运算符(~)

取反很简单,就是二进制中的0变1,1变0。

~10 = ?   // 10的取反是多少?

       00001010 -> 10
    ~  
    --------------------
       -> -11  --> 由于符号位是1,负数,这里是补码,得转换为原码才行。


~(-10) = ?  // -10的取反是多少?

       -> -10  这里存储的是-10的补码,原码是
    ~
    --------------------
       00001001 -> 9


 
  

结果如下:

常见综合案例

1、计算一个二进制数里有几个1?

我们用char类型举例,1个字节,方便查看结果。

 
  

显示结果:

代码解析:

for (int i = 0; i < 8; i++)
{
        char a = (val >> i) & 1;
        if (a == 1) { num++; }
        arr[i] = a;
}




以-23举例,二进制是,因为是负数,所以内存中存储的是补码(原码取反+1),二进制的位运算没有遍历功能,因此我们将该二进制依次向右移1位,然后和1进行与(&)运算,将其前面的位都置为0,结果就是第1位上的值了,共右移8次即可。

>> 0
得到 
   &   00000001
-----------------------
         00000001 --->这就是第1位上的值。   



二进制就是这么遍历的。

2、消失的数字

 
  

代码解析:

这是关于异或的一道典型题,解题的原理是利用
0 ^ a = a        // 0与任何数异或结果都是任何数
a ^ b ^ a = b   // a ^ b ^ a = a ^ a ^ b = b,乘法的交换律

    int arr[9] = {9,4,7,1,6,5,2,3,0};
    int x = 0;
    for (int i = 0; i < 9; i++)
    {
        x = x ^ arr[i] ^ i;
    }
    x ^= 9;





x = x ^ arr[i] ^ i; // 这个是重点也是难点
 我们先不要每次循序都计算x的值,把式子都保留在一起最后计算,每次循环后积累的式子结果如下:

x = 0 ^ 9 ^ 0 ^ 4 ^ 1 ^ 7 ^ 2 ^ 1 ^ 3 ^ 6 ^ 4 ^ 5 ^ 5 ^ 2 ^ 6 ^ 3 ^ 7 ^ 0 ^ 8 ^ 9   // 补充x ^= 9;因为数组只有9个数,差1个。

上面是数组的9次循环再补充 ^= 9的式子展开结果。每次循环用不用颜色标记一下能看得清楚些。

异或就是消消乐,把相同的消掉,结果就是

x = 0 ^ 8 = 8  // OK

版权声明


相关文章:

  • .io是什么意思2025-07-06 19:30:04
  • mfcc特征提取代码2025-07-06 19:30:04
  • vue开头怎么做2025-07-06 19:30:04
  • 实现大小端判断2025-07-06 19:30:04
  • 软件安全性测试应该从哪些方面考虑2025-07-06 19:30:04
  • linux系统赋予文件权限命令2025-07-06 19:30:04
  • 数据库事务性2025-07-06 19:30:04
  • unity不同版本区别2025-07-06 19:30:04
  • 嵌入式 存储2025-07-06 19:30:04
  • java中内部类的定义2025-07-06 19:30:04