浅析位运算符(左移、右移、与、或、异或)

        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\n", r1);    // 20 (10*2的1次方)

结果如下:

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


      结果如下:

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

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

结果如下:

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

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

完整代码如下:

int main()
{
	char ch = 10;
	
	// 左移1位
	char r1 = ch << 1;
	printf("10 << 1 = %d\n", r1);

	// 左移2位
	char r2 = ch << 2;
	printf("10 << 2 = %d\n", r2);

	// 左移3位
	char r3 = ch << 3;
	printf("10 << 3 = %d\n", r3);

	// 左移4位
	char r4 = ch << 4;
	printf("10 << 4 = %d\n", r4);

	// 左移4位
	short r44 = ch << 4;
	printf("10 << 4 = %d\n", r44);


	return 0;
}
2、负数左移举例:

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

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

结果如下:

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

结果如下:

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

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

结果如下:

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

结果如下:


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

完整代码如下:

int main()
{
	char ch = -10;
	
	// 左移1位
	char r1 = ch << 1;
	printf("-10 << 1 = %d\n", r1);

	// 左移2位
	char r2 = ch << 2;
	printf("-10 << 2 = %d\n", r2);

	// 左移3位
	char r3 = ch << 3;
	printf("-10 << 3 = %d\n", r3);

	// 左移4位
	char r4 = ch << 4;
	printf("-10 << 4 = %d\n", r4);

	// 左移4位
	short r44 = ch << 4;
	printf("-10 << 4 = %d\n", r44);

	return 0;
}

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

二、右移运算符(>>)

        右移运算就是将一个二进制位的操作数按指定位数整体向右移位,移出位被丢弃,左边的空位补符号位(还有一种情况是无论正负数都补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\n", r1);    // 5 (10/2的1次方)
        // 右移2位
        char r2 = ch >> 2; // 00000010
        printf("10 >> 2 = %d\n", r2);    // 2 (10/2的2次方,这里小数丢失精度了)
        // 右移3位
        char r3 = ch >> 3; // 00000001
        printf("10 >> 3 = %d\n", r3);    // 1 (10/2的3次方)
        // 右移4位
        char r4 = ch >> 4; // 00000000
        printf("10 >> 4 = %d\n", r4);    // 0
        // 哪怕是我们用2个字节的short来接收结果也是一样的。
        // 右移4位
        short r44 = ch >> 4; // 00000000
        printf("10 >> 4 = %d\n", r44);    // 0

结果如下:

完整代码

int main()
{
	char ch = 10;
	
	// 右移1位
	char r1 = ch >> 1;
	printf("10 >> 1 = %d\n", r1);

	// 右移2位
	char r2 = ch >> 2;
	printf("10 >> 2 = %d\n", r2);

	// 右移3位
	char r3 = ch >> 3;
	printf("10 >> 3 = %d\n", r3);

	// 右移4位
	char r4 = ch >> 4;
	printf("10 >> 4 = %d\n", r4);

	// 右移4位
	short r44 = ch >> 4;
	printf("10 >> 4 = %d\n", r44);

	return 0;
}
2、负数右移举例:

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

结果如下:

完整代码:

int main()
{
	char ch = -10;

	// 右移1位
	char r1 = ch >> 1;
	printf("-10 >> 1 = %d\n", r1);

	// 右移2位
	char r2 = ch >> 2;
	printf("-10 >> 2 = %d\n", r2);

	// 右移3位
	char r3 = ch >> 3;
	printf("-10 >> 3 = %d\n", r3);

	// 右移4位
	char r4 = ch >> 4;
	printf("-10 >> 4 = %d\n", r4);

	// 右移4位(强转)
	char r44 = (unsigned char)ch >> 4;
	printf("(unsigned char)-10 >> 4 = %d\n", r44);

	// 负数无论右移多少次,-1也就是到头了啊!
	short a = -1207;
	printf("-1207 >> 100 = %d\n", -1207 >> 100);

	return 0;
}

三、与运算符(&)

在编程中我们经常用到与运算符(&&),比如下面的代码:
if (1==1 && 2==2)
{
        printf("这里用到与运算符,二者都为真才是真,否则为假!\n");
}
这个与运算符(&&)是针对字节而言的,要是针对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
    
    代码如下:

int main()
{
	char a = 10;
	char b = 3;
	char c = a & b;
	printf("10 & 3 = %d\n", c);

	return 0;
}

结果如下:


那么与(&)到底有什么用呢?
我们可以把二进制中的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   【11100110,这里用一个字节来演示,最高位为1,则该数是负数,是补码,需要转变为原码才行。但如果我们用16进制E6来表示该数则不需要考虑负数的事,因此很多代码都是用16进制来进行与运算的。】
      -------------------------------------------
          0 1 0 0 0 1 0 0  -> 68   -> 44

int main()
{
	// 10进制进行与运算
	char a = 77;
	char b = -26;
	char c = a & b;
	printf("10进制与运算:%d\n", c);
	
	// 16进制进行与运算
	char a1 = 0x4D;
	char b1 = 0xE6;
	char c1 = a1 & b1;
	printf("16进制与运算:%X\n", c1);

	return 0;
}

结果如下:

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

四、或运算符(|)

    在编程中我们经常用到或运算符(||),比如下面的代码:
    if(1==1 || 2==3)
    {
        printf("这里用到或运算符,二者只要有一个为真结果就是真,否则为假!\n");
    }
    这个或运算符(||)是针对字节而言的,要是针对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

int main()
{
	char a = 10;
	char b = 3;
	char c = a | b;
	printf("10 | 3 = %d\n", c);

	return 0;
}

    结果如下:

    那么或(|)到底有什么用呢?
    我们可以把二进制中的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   【10100010,这里用一个字节来演示,最高位为1,则该数是负数,是补码,需要转变为原码才行。但如果我们用16进制来A2表示该数则不需要考虑负数的事,因此很多代码都是用16进制来进行或运算的。】
      ----------------------------------
         1 1 1 0 1 1 1 1  -> -17   -> EF  【11101111是负数的补码,转换为原码是-17】

    代码如下:

int main()
{
	// 10进制进行或运算
	char a = 77;
	char b = -94;
	char c = a | b;
	printf("%d\n", c);

	// 16进制进行或运算
	char a1 = 0x4D;
	char b1 = 0xA2;
	char c1 = a1 | b1;	// 11101111
	printf("%X\n", (unsigned char)c1);	// c1结果的符号位是1,表示负数,正常printf()输出是4个字节,那么另外3个字节会自动补符号位,即 11111111 11111111 11111111 11101111 -> FFFFFFEF,如果我们只想显示1个字节可以强转为unsigned char类型,变为无符号char型,不影响显示。

	return 0;
}

结果如下:

这里特意将符号位数字改为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
 

int main()
{
	char a = 10;
	char b = 3;
	char c = a ^ b;
	printf("10 ^ 3 = %d\n", c);

	return 0;
}

结果如下:

    就是这么简单。
    异或运算有一些很有意思的规律:
    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个临时变量来帮忙)。

int main()
{
	int a = 10;
	int b = 20;
	printf("交换前:a=%d b=%d\n", a, b);
	a = a ^ b;
	b = a ^ b;
	a = a ^ b;
	printf("交换后:a=%d b=%d\n", a, b);

	return 0;
}

结果如下:

        解析一下上面的代码,这里用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、利用异或加解密

int main()
{
	// 原理:pwd ^ key ^ key = pwd ^ (key ^ key) = pwd ^ 0 = pwd
	char pwd[] = "Lag234!@#";
	char key = 100;		// 秘钥
	printf("加密前的密码:%s\n", pwd);
	int len = strlen(pwd);
	for (int i = 0; i < len; i++)
	{
		pwd[i] ^= key;	// pwd ^ key
	}
	printf("加密后的密码:%s\n", pwd);
	for (int i = 0; i < len; i++)
	{
		pwd[i] ^= key;	// pwd ^ key ^ key 
	}
	printf("解密后的密码:%s\n", pwd);

	return 0;
}

结果如下:

六、取反运算符(~)

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

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

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

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

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

int main()
{
	char a = 10;
	char b = -10;

	printf("~10 = %d\n",~a);
	printf("~(-10) = %d\n", ~b);

	return 0;
}

结果如下:

常见综合案例

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

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

#include <stdio.h>

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

	for (int i = 7; i >= 0; i--)
	{
		printf("%d ",arr[i]);
	}

	return num;
}

int main()
{
	printf("请输入一个数值(-128到127):");
	int val;
	scanf("%d",&val);
	int num = bitOneNum((char)val);
	printf("\n您输入的数值是%d,它的二进制码包含 %d 个1。\n",val,num);

	return 0;
}

显示结果:

代码解析:

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

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

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

二进制就是这么遍历的。

2、消失的数字

有0到9连续10个数字,下面的数中好像丢了1个,你知道是哪个吗?
[9,4,7,1,6,5,2,3,0]

// 消失的数字
int main()
{
	// 有0到9连续10个数字,下面的数组中好像丢了1个,你知道是哪个吗?
	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;

	printf("丢失的数字是:%d\n",x);

	return 0;
}

代码解析:

这是关于异或的一道典型题,解题的原理是利用
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

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
C语言中的位运算符包括左移运算符(<<)、右移运算符(>>)、按位与运算符(&)、按位或运算符(|)和按位异或运算符(^)。其中左移运算符右移运算符可以用来对一个数进行位移操作。无符号数的左移右移是指将一个无符号数的二进制表示向左或向右移动指定的位数,移动后的空位用0填充。下面是一个无符号数左移的例子: 引用:例子二:无符号数的左移 #include <stdio.h> int main(){ unsigned int a = 0x80000001; int i=0; for(;i<64;i++){ printf("left %d:%08x,%u\n",i,a<<i,a<<i); } } 在这个例子中,我们定义了一个无符号整型变量a,并将其初始化为0x80000001。然后我们使用for循环对a进行了64次左移操作,每次左移的位数从0到63。在每次左移后,我们使用printf函数输出了左移的位数、左移后的结果以及结果的十进制表示。可以看到,每次左移后,a的值都会乘以2的移动位数次方。 无符号数的右移是指将一个无符号数的二进制表示向右移动指定的位数,移动后的空位用0填充。下面是一个无符号数右移的例子: 引用:例子四:无符号数的右移 #include <stdio.h> int main(){ unsigned int a = 0x10000001; int i=0; for(;i<64;i++){ printf("left %d:%08x,%u\n",i,a>>i,a>>i); } } 在这个例子中,我们定义了一个无符号整型变量a,并将其初始化为0x10000001。然后我们使用for循环对a进行了64次右移操作,每次右移的位数从0到63。在每次右移后,我们使用printf函数输出了右移的位数、右移后的结果以及结果的十进制表示。可以看到,每次右移后,a的值都会除以2的移动位数次方。 需要注意的是,对于有符号数的左移右移,如果移动后的空位用符号位填充,那么就会出现符号位溢出的问题。例如,如果将一个有符号整型变量i的值左移3位,那么移动后的空位将用符号位填充。如果i的值为8,那么左移3位后,i的值将变为64,而不是24。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值