代码实例看透位运算符 | ^ ~

张开发
2026/4/13 15:08:41 15 分钟阅读

分享文章

代码实例看透位运算符  | ^ ~
要先理解原码补码反码可以看这个文章https://blog.csdn.net/2301_80428740/article/details/147284230?spm1011.2415.3001.10575sharefrommp_manage_link 在C语言中位运算符是直接操作变量二进制补码的“底层操作工具”核心作用是对两个或一个变量的二进制位逐位进行运算。它比移位运算符更灵活在数据处理、状态判断等场景中经常用到。今天我们就结合具体代码实例用通俗的语言拆解最常用的4个位运算符按位与、按位或|、按位异或^、按位取反~。先明确3个核心前提避免理解偏差一是所有位运算操作的都是变量的二进制补码计算机存储数据的真实形式二是运算后原变量的值不会改变只会生成新的运算结果三是除了按位取反~是单目运算符只需要一个操作数其余、|、^都是双目运算符需要两个操作数运算时会将两个操作数的二进制位按“对应位置”逐位运算。例子代码// | ^ ~ 位运算符 #include stdio.h int main() { int num1 3; int num2 -5; printf(%d\n, num1 num2); // 00000000000000000000000000000011 3的补码 // 10000000000000000000000000000101 -5的原码 //求-5的补码 // 11111111111111111111111111111010 取反 // 11111111111111111111111111111011 加1 -5的补码 //按位与全1才为1否则为0 // 00000000000000000000000000000011 3的补码 // 11111111111111111111111111111011 -5的补码 // 00000000000000000000000000000011 按位与后的补码 //发现符号位为0说明是正数所以结果为3。如果是 负数还需要再取反加1一次。 printf(%d\n, num1 | num2); printf(%d\n, num1 ^ num2); // ~3 结果验证代码 int x -3; int y ~x; printf(~3 的结果是%d\n, y); // 最终输出-4 printf(~-6的结果是%d\n, ~- 6); // 最终输出5 printf(%d\n, ~0);//特殊 return 0; }一、按位与全1才为1否则为0按位与的语法操作数1 操作数2核心规则只有一条将两个操作数的二进制补码按对应位对齐只有当对应两位都为1时结果位才是1只要有一位是0结果位就是0可以记为“同1为1一0则0”。我们直接看代码中的核心案例num13num2-5计算num1 num2int num1 3; int num2 -5; printf(%d\n, num1 num2); // 最终输出3要理解这个结果必须先明确两个数的二进制补码再逐位运算步骤拆解如下第一步求3的二进制补码。3是正数原码、反码、补码完全相同32位二进制为 00000000 00000000 00000000 00000011第二步求-5的二进制补码。负数的补码需要通过“原码→反码→补码”的步骤计算1. 原码最高位符号位为1其余位为数值的二进制即 10000000 00000000 00000000 000001012. 反码符号位不变其余位按位取反0变11变0即 11111111 11111111 11111111 111110103. 补码反码加1即 11111111 11111111 11111111 11111011第三步按位与运算。将两个补码的对应位对齐逐位应用“全1才为1”的规则num1补码 00000000 00000000 00000000 00000011num2补码 11111111 11111111 11111111 11111011逐位运算结果00000000 00000000 00000000 00000011第四步将运算结果的补码转换为十进制。运算结果的补码最高位是0说明是正数补码即原码对应十进制就是3——这就是代码中printf输出3的原因。补充一个实用场景按位与常用来“提取指定二进制位”或“将指定位清0”。比如要提取一个数的最低4位只需让它和00001111十进制15做按位与运算即可。二、按位或|有1就为1全0才为0按位或的语法操作数1 | 操作数2核心规则和按位与相反将两个操作数的二进制补码按对应位对齐只要对应两位中有一位是1结果位就是1只有当两位都为0时结果位才是0可以记为“一1则1同0为0”。我们继续用代码中的num13和num2-5计算num1 | num2代码中printf(%d\n, num1 | num2);步骤拆解如下第一步明确两个数的补码和按位与案例一致直接复用 num1补码00000000 00000000 00000000 00000011 num2补码11111111 11111111 11111111 11111011第二步按位或运算。逐位应用“有1就为1”的规则 num1补码00000000 00000000 00000000 00000011 num2补码11111111 11111111 11111111 11111011 逐位运算结果11111111 11111111 11111111 11111011第三步将运算结果的补码转换为十进制。运算结果的补码最高位是1说明是负数需要先转换为原码补码取反加1 1. 补码取反10000000 00000000 00000000 00000100 2. 加1得到原码10000000 00000000 00000000 00000101 原码对应十进制是-5所以num1 | num2的结果是-5。补充实用场景按位或常用来“将指定二进制位置1”。比如要将一个数的最低位设为1只需让它和00000001十进制1做按位或运算即可。三、按位异或^不同为1相同为0按位异或的语法操作数1 ^ 操作数2核心规则很有特点将两个操作数的二进制补码按对应位对齐当对应两位不同时一个0、一个1结果位就是1当对应两位相同时都是0或都是1结果位就是0可以记为“异则1同则0”。还是用num13和num2-5计算num1 ^ num2代码中printf(%d\n, num1 ^ num2);步骤拆解如下第一步复用两个数的补码num1补码00000000 00000000 00000000 00000011num2补码11111111 11111111 11111111 11111011第二步按位异或运算。逐位应用“不同为1相同为0”的规则num1补码00000000 00000000 00000000 00000011num2补码11111111 11111111 11111111 11111011逐位运算结果11111111 11111111 11111111 11111000第三步将运算结果的补码转换为十进制。结果补码最高位是1为负数转换为原码取反加11. 补码取反 10000000 00000000 00000000 000001112. 加1得到原码10000000 00000000 00000000 00001000原码对应十进制是-8所以num1 ^ num2的结果是-8。补充两个实用场景1. 按位异或可以实现“不借助临时变量交换两个数”a a ^ b; b a ^ b; a a ^ b;2.一个数和0做异或运算结果还是它本身一个数和自己做异或运算结果是0相同为0。四、按位取反~0变11变0按位取反是唯一的单目位运算符语法~ 操作数核心规则最简单将操作数的二进制补码每一位都翻转包括符号位——0变成11变成0可以记为“逐位翻转”。1 正整数按位取反结果必然是负数符号位从 0 翻转为 1结果 正整数的相反数 - 1通用公式~n -n - 1n 为正整数看代码中的案例计算~3int x 3; int y ~x; printf(~3 的结果是%d\n, y); // 最终输出-4~3的结果应该是-43的相反数是-3-3-1-42 负整数按位取反结果必然是非负数符号位从 1 翻转为 0结果 负整数的相反数 - 1通用公式仍成立~n -n - 1n 为负整数看代码中的案例计算~-6printf(~-6的结果是%d\n, ~- 6); // 最终输出53 0的按位取反是-1特殊printf(%d\n, ~0); // 最终输出-1步骤拆解如下第一步明确0的32位二进制补码。0的原码、反码、补码都是全0 00000000 00000000 00000000 00000000第二步按位取反运算。将每一位0都变成1得到 11111111 11111111 11111111 11111111第三步将运算结果的补码转换原码。1. 补码取反10000000 00000000 00000000 000000002. 加1得到原码10000000 00000000 00000000 00000001 原码对应十进制是-1所以~0的结果是-1。五、核心规则总结表必记运算符类型核心规则简单记忆按位与双目对应两位全1则为1否则为0同1为1一0则0|按位或双目对应两位有1则为1全0则为0一1则1同0为0^按位异或双目对应两位不同则为1相同则为0异则1同则0~按位取反单目每一位都翻转0变1、1变0逐位翻转练习题1不创建第三个变量实现两个整数的交换^方法一#includestdio.h int main() { int a 10; int b 20; printf(交换前a %d, b %d\n, a, b); // a 10, b 20 aab; b a - b; a a - b; printf(交换后a %d, b %d\n, a, b); // a 20, b 10 return 0; }方法二#includestdio.h int main() { int a 10; int b 20; printf(交换前a %d, b %d\n, a, b); // a 10, b 20 a a ^ b; b a ^ b; a a ^ b; printf(交换后a %d, b %d\n, a, b); // a 20, b 10 return 0; }关键特性也是这段代码能生效的基础自反性x ^ x 0任何数和自己异或结果为 0归零性x ^ 0 x任何数和 0 异或结果还是自己交换律 / 结合律a ^ b b ^ a、(a ^ b) ^ c a ^ (b ^ c)逐行拆解代码执行过程我们以初始值a10二进制00001010、b20二进制00010100为例一步步算第一步a a ^ b;计算10 ^ 20 00001010 ^ 00010100 00011110十进制30此时a 30b 20b 还没变化第二步b a ^ b;把第一步的a30代入本质是b (a原来 ^ b原来) ^ b原来根据异或结合律 自反性(a ^ b) ^ b a ^ (b ^ b) a ^ 0 a计算30 ^ 20 00011110 ^ 00010100 00001010十进制10此时a 30b 10b 已经变成了原来的 a第三步a a ^ b;此时的a30原 a^ 原 bb10原 a本质是a (a原来 ^ b原来) ^ a原来同理(a ^ b) ^ a b ^ (a ^ a) b ^ 0 b计算30 ^ 10 00011110 ^ 00001010 00010100十进制20此时a 20b 10a 变成了原来的 b最终实现了a和b的交换整个过程没有用到临时变量完全靠异或的特性完成。对比加减法交换 vs 异或交换新手必看特性加减法交换异或交换适用类型整数 / 浮点数仅整数二进制位运算风险点可能溢出比如 ab 超出 int 范围仅当 a 和 b 指向同一地址时出错底层逻辑加减法逆运算二进制位异或特性可读性更高更易理解较低需懂异或规则练习题2 求一个整数在内存中的二进制中1的个数这里我们将用到按位与运算符因为它的“全1才为1”规则能精准判断某一位是否为1。方法一取余除2法新手友好利用整数除以2的特性一个整数除以2本质是其二进制补码向右移动1位。通过“取余2”判断当前最右边的位是否为1若n%21则最右边位是1若n%20则是0统计完最右边位后将n除以2舍弃该位重复操作直到n变成0统计的次数就是1的个数。#includestdio.h int main() { int n 0; int count 0; scanf(%d, n); while (n ! 0) { if (n % 2 1) { count; } n n / 2; } printf(%d, count); return 0; }适用场景正数统计逻辑直观无需理解位运算细节 - 局限性 1. 无法直接统计负数负数在内存中存储的是补码全1开头如-1在内存中是全1而我们测试得到的结果为0可改进改为无符号类型编译器-1的全1认为正数有32个1unsigned int n 0; // 无符号整数方法二逐位右移1法逻辑清晰兼容正负负数的符号位也算1核心逻辑基于“右移运算”和“按位与运算”的结合1. 右移运算n i将n的二进制补码向右移动i位第i位从0开始计数从右往左会移动到最右边的位置2. 按位与运算11的二进制补码是“...0001”只有最右边一位是1。用右移后的结果与1做按位与若结果为1说明当前移动到最右边的位是1若为0则是0 3. 循环32次针对32位整数依次判断每一位是否为1统计1的总数。代码实现#includestdio.h int main() { int n 0; //思路 // n13 // 1101 // 0001 按位与 1101 0001 0001 可能得出最后一位是不是1 // nn1 n0110 // 0110 0001 0000 可能得出倒数第二位是不是1 int count 0; scanf(%d, n); for (int i 0; i 32; i) { if (((n i) 1) 1) { count; } } printf(%d, count); return 0; }代码中需注意运算符优先级右移运算符和按位与运算符的优先级低于比较运算符因此必须给“(n i) 1”加上括号否则会先计算“i 1”导致逻辑错误。方法三n (n-1)法高效简洁对于任意整数n执行n (n-1)运算会将n的二进制补码中最右边的一个1变成0其他位不变。每执行一次该运算就统计一个1直到n变成0所有1都被统计完循环的次数就是二进制中1的个数。举个例子理解n6 补码00000000 00000000 00000000 00000110n-15补码00000000 00000000 00000000 00000101执行n(n-1)00000000 00000000 00000000 00000100 4最右边的1第2位被变成0再执行一次n4n-134300000000 00000000 00000000 00000000最右边的1第3位被变成0。// 统计整数n的二进制补码中1的个数 int countOneBits(int n) { int count 0; // 用于计数1的个数 while (n ! 0) { // 当n为0时所有1都已统计 n n (n - 1); // 每次将最右边的1变成0 count; // 统计次数加1 } return count; } int main() { int n 0; scanf(%d, n); printf(%d, countOneBits(n)); return 0; }三种方法核心对比表对比维度取余除2法逐位右移1法n (n-1)法核心逻辑取余判断最右位除2舍弃最右位右移i位后与1按位与判断第i位是否为1n(n-1)清0最右1统计清0次数是否支持负数不支持会陷入死循环支持固定循环32次覆盖所有位支持直接操作补码循环效率低循环次数二进制位数最多32次中固定循环32次高循环次数1的个数最少1次理解难度低依赖基础算术运算中需理解右移和按位与的结合中高需理解n(n-1)清0最右1的特性适用人群新手刚接触整数二进制表示有基础的学习者理解移位运算掌握位运算核心特性的学习者/开发者练习题三判断整数是否为 2 的 n 次方方法1#includestdio.h int main() { int a 0; scanf(%d, a); // 核心判断逻辑 // 1. 必须是正数2的n次方不可能≤0 // 2. a (a-1) 02的n次方二进制仅1个1减1后低位全1按位与结果为0 if (a 0 (a (a - 1)) 0) { printf(%d 是2的n次方\n, a); } else { printf(%d 不是2的n次方\n, a); } return 0; }如16 10000 01111 00000方法二#includestdio.h int main() { int a 0; int count 0; scanf(%d, a); while (a ! 1 a%20) { a (a 1) / 2; } printf(a 1 ? 是2的n次方\n : 不是2的n次方\n); return 0; }是 2 的 n 次方的数除2的余数不等于0且最后除2等于1练习4 一个数的某位二进制置0或者置1#include stdio.h int main() { int a 13; a a | (1 4); printf(a %d\n, a); a a ~(1 4); printf(a %d\n, a); return 0; }掩码Mask专门构造的一个数用来和目标数做位运算实现对指定位的操作。置 1 操作用num | (1 n)核心是按位或 掩码第 n 位为 1目标位强制为 1其他位不变。置 0 操作用num ~(1 n)核心是按位与 掩码第 n 位为 0目标位强制为 0其他位不变。练习5 单身狗问题在一个整型数组中只有一个数字出现一次其他数组都是成对出现的请找出那个只出现一次的数字。例如数组中有1 2 3 4 5 1 2 3 4只有5出现一次其他数字都出现2次找出5//单身问题 #include stdio.h int main() { int a [] {1,2,3,4,1,2,3}; int b 0; for (int i 0; i sizeof(a) / sizeof(a[0]); i) { b^a[i]; } printf(%d\n, b); }思路核心原理异或运算中相同数异或00异或任何数该数本身解题逻辑用 0 依次异或数组所有元素成对出现的数会相互抵消结果为 0最终剩下的就是唯一出现一次的数代码执行0 异或 1→异或 2→异或 3→异或 4→异或 1→异或 2→异或 3 (1^1)^(2^2)^(3^3)^4 0^0^0^4 4。练习6 打印整数二进制的奇数位和偶数位#includestdio.h int main() { int a 0; scanf(%d, a); int b a; //偶数位清0 for (int i 1; i 32; i 2) { b b ~(1 i); } for (int i 31;i 0; i--) { printf(%d, (b i) 1); } printf(\n); //奇数位清0 int c a; for (int j 0; j 32; j 2) { c c ~(1 j); } for (int j 31; j 0; j--) { printf(%d, (c j) 1); } printf(\n); return 0; }

更多文章