本篇博客会讲解C语言中的6个位操作符:按位取反(~)、按位与(&)、按位或(|)、按位异或(^)、左移(<<)、右移(>>)。这6个操作符都是操作整数的二进制位的。在学习这6个位操作符之前,大家需要先掌握“整数在内存中的存储”这个知识点,详见我的上一篇博客:戳这里跳转。
按位取反(~)
按位取反操作符(~)是一个一元操作符,用于对一个整数的每一位进行取反操作。具体来说,它会将每一位的0变为1,1变为0。
举个例子:
int a = 0;
b = ~a;
// 此时a、b分别是多少?
当a存储的是0时,内存中存放的是补码,即32位全是0:00000000000000000000000000000000
,按位取反之后就变成全1了,即11111111111111111111111111111111
。由于还在内存中放着,就还是补码,转换成原码后就得到10000000000000000000000000000001
,即-1。所以此时b就得到了-1,但是按位取反操作符并没有改变a,所以a还是0。
按位与(&)
按位与操作符(&)是一种二元操作符,用于对两个整数进行按位与运算。按位与操作符将两个整数补码的二进制进行按位比较,只有在相应位置上都为1时,结果才为1,否则结果为0。
例如,假设有两个整数a和b,它们在内存中存储的二进制(其实就是补码)分别为:
a = 00000000000000000000000010101100
b = 00000000000000000000000011001010
那么a & b的结果为:
a & b = 00000000000000000000000010001000
举一个简单的例子:判断一个数的奇偶性。你肯定知道这种判断方式:
if (n%2 == 1)
{
// 奇数
}
else
{
// 偶数
}
但也可以这么写:
if (n&1 == 1)
{
// 奇数
}
else
{
// 偶数
}
这是因为,1的补码的二进制是:00000000000000000000000000000001
,如果拿一个数和它按位与,左边的31位全部会变成0。最右边的那一位如果是1,按位与之后就得到1;最右边的那一位如果是0,按位与之后就得到0。另外,奇数的最右边一位就是1,偶数最右边一位是0,这就对应上了。
也就是说:n&1
这个式子能拿到n最右边的那一位。n最右边的那一位是1,n&1
就是1;n最右边的那一位是0,n&1
就是0。这一点非常重要!
当然,按位与还有一个作用:可以把某一位清0。比如,如果想把n的最右边一位清0,就n &= 0xfffffffe
即可,也就是让n按位与11111111111111111111111111111110
这样一个二进制。n左边的31位按位与1后不变,最右边1位不管是0还是1,按位与0后都得到0。
按位或(|)
按位或操作符(|)是一个二元操作符,用于对两个整数进行按位或运算。按位或操作符将两个整数补码的二进制进行按位比较,只有在相应位置上都为0时,结果才为0,否则结果为1。
例如,假设有两个整数a和b,它们在内存中存储的二进制(其实就是补码)分别为:
a = 00000000000000000000000010101100
b = 00000000000000000000000011001010
那么a | b的结果为:
a | b = 00000000000000000000000011101110
按位异或(^)
按位异或操作符(^)是一种二元操作符,用于对两个整数进行按位异或运算。按位异或运算是指对两个二进制数的每一位进行比较,如果相同则结果为0,如果不同则结果为1。可以简记为“找不同”。
详细点来说,就是以下的规则:
- 如果两个操作数的某一位都为0,则结果的该位也为0。
- 如果两个操作数的某一位都为1,则结果的该位也为0。
- 如果两个操作数的某一位一个为0,一个为1,则结果的该位为1。
有了按位与和按位或的基础,大家应该很好理解了。举个例子:
int a = 10; // 补码为1010
int b = 6; // 补码表示为0110
int c = a ^ b; // 按位异或运算,结果为1100,即12
在上面的代码中,变量a和b分别存储了10和6的补码,分别为00000000000000000000000000001010
和00000000000000000000000000000110
。对这两个数进行按位异或运算,得到的结果为00000000000000000000000000001100
,即12的补码。
掌握按位异或需要明白2个道理:
- 0^a=a, a^a=0
- 按位异或可以任意交换顺序。把同样的一堆数按位异或到一块,不管怎么交换这几个数的顺序,结果是不变的。
使用按位异或有一个很经典的例子:不创建临时变量,交换2个整数。
假设有2个整数:
int a = 10;
int b = 20;
如果创建临时变量,就这么写:
int tmp = a;
a = b;
b = tmp;
如果不创建临时变量,可以使用加减法交换:
a = a + b;
b = a - b;
a = a - b;
加减法交换的思路是:
- 先把2个数的和存在a中。
- 和减去b,得到最开始的a,放在b中。
- 和减去b中存放的最开始的a,得到最开始的b,放在a中,从而完成交换。
或者使用按位异或交换:
a = a ^ b;
b = a ^ b;
a = a ^ b;
思路是:
- 先把2个数异或的结果(a^b)放在a中。
- 这个结果异或b,得到
(a^b)^b=a
,放在b中(根据按位异或的2条性质可以推出这一点,朋友们开动脑筋想一想,忘记的可以向上翻)。 - 这个结果异或b中存储的一开始的a,得到b,放在a中,从而完成交换。
左移(<<)
C语言中的左移操作符(<<)是一种位运算符,它将一个数的二进制表示向左移动指定的位数,并在右侧填充零,简记为:左边丢弃,右边补0。左移操作符的语法是:value << n
,其中,value是要进行左移操作的数,n是要左移的位数。
例如,假设value的二进制表示为00000000000000000000000000001010
,执行value << 2
操作后,结果为00000000000000000000000000101000
,即将原来的二进制数向左移动了两位,最左边的2位丢了,右侧填充了两个零。
左移操作符的作用是将一个数乘以2的n次方,因为左移n位相当于将原数乘以2的n次方。
需要注意的是,左移操作符可能会导致数据溢出,因此在使用时需要谨慎。如果左移的位数超过了数据类型的位数,那么结果将是未定义的。此外,左移操作符如果作用于负数,会导致负数的符号位被丢弃。
右移(>>)
右移操作符(>>)是一种位运算符,用于将一个数的二进制补码向右移动指定的位数。右移操作符有两种类型:算术右移和逻辑右移。
- 算术右移(右边丢弃,左边补原来的符号位):当对整数进行右移操作时,算术右移会将最高位的符号位保留下来,即将符号位复制到右移后的空位上。例如,对于这样一个二进制序列
11111111111111111111111111111111
,进行算术右移1位,结果仍然是11111111111111111111111111111111
,因为符号位1被复制到右移后的空位上。 - 逻辑右移(右边丢弃,左边补0):当对整数进行右移操作时,逻辑右移会将最高位的符号位忽略掉,即将右移后的空位上填充0。例如,对于这样一个二进制序列
11111111111111111111111111111111
,进行逻辑右移1位,结果为01111111111111111111111111111111
,因为右移后的空位无论如何都会补0。
需要注意的是,右移操作符可能会导致数据溢出,因此在使用时需要谨慎。如果右移的位数超过了数据类型的位数,那么结果将是未定义的。此外,左移操作符如果作用于负数,算术右移和逻辑右移的结果是不一样的,算术右移会在高位补符号位(即1),逻辑右移会在高位补0。
总结
- 按位取反操作符(~)用于对内存中存储的二进制的每一位进行逻辑取反操作(!)。
- 按位与操作符(&)用于对内存中存储的二进制补码的每一位进行逻辑与运算(&&),按位或操作符(|)用于对内存中存储的二进制补码的每一位进行逻辑或运算(||)。
- 按位异或操作符(^)可以理解为“找不同”,如果两个整数在内存中存储的二进制补码的对应位不同,则结果为1,否则为0。
- 左移操作符(<<)的规则是:左边丢弃,右边补0。右移操作符(>>)的规则分为2种,算术右移的规则是:右边丢弃,左边补原来的符号位;逻辑右移的规则是:右边丢弃,左边补0。
感谢大家的阅读!