news 2026/10/3 3:57:14

C语言经典100例第53题:彻底搞懂按位异或运算

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言经典100例第53题:彻底搞懂按位异或运算

动手练C语言的人,应该都撞过菜鸟教程C经典100例这套题。前面52题练的无非是循环、数组、函数这些常规操作,到了第53题画风突变——题目要你“学习使用按位异或 ^”,然后甩给你一段极简代码。我第一次跑这段代码的时候,屏幕上安安静静打出两个数字:60 和 59。说实话,当时我盯着那个077看了半天,心想这不是77吗?怎么异或一个3就变成了60?后来又异或一个7,又变成59?那一瞬间我才意识到,自己对八进制、二进制、位运算这些概念还停留在“背过但没真正用过的状态”。这一题等于直接把我按在原地,逼我把二进制的底裤翻出来看。

这篇文章就把练习53的完整拆解、异或运算的核心原理、以及从这道题延伸出去的实战用法全部捋一遍。如果你正在刷这套C经典100例,或者刚刚接触位运算,这篇应该能帮你省不少时间。

1. 先跑一遍:练习53的原始代码和输出

1.1 题目原文与配套源码

在菜鸟教程C经典100例中,第53题位于位运算专题的中间位置。第51题讲的是按位与&,第52题讲的是按位或|,到了第53题,就开始碰按位异或^了。题目描述极简:

题目:学习使用按位异或 ^。
程序分析:0^0=0; 0^1=1; 1^0=1; 1^1=0

配套源码也经典到不能再经典:

#include <stdio.h> int main() { int a, b; a = 077; b = a ^ 3; printf("b = %d \n", b); b ^= 7; printf("b = %d \n", b); return 0; }

就这么几行,没有函数封装、没有输入交互、没有注释。它摆明了是一道“读代码、跑代码、理解代码”的练习。

1.2 本机实跑结果

我本机是 Ubuntu 22.04 + GCC 11.4,直接gcc ex53.c && ./a.out编译运行,输出:

b = 60 b = 59

如果你用的是 Windows + MinGW,或者 macOS + Clang,结果完全一样。因为这几个整数都很小,无论int是16位还是32位,结果都符合C标准定义,不依赖操作系统和编译器实现。

1.3 输出结果逐字拆解:077 不是77

这道题最大的坑,也是很多新手最容易卡住的地方,就是077这个写法。

C语言里有一条容易被忽略的规则:整数常量如果以0开头,表示它是八进制数。也就是说,077不是一个写了前导零的77,它表示的是八进制的77,换算成十进制是:

7 * 8 + 7 = 63

再写成二进制,就是0b00111111,也就是低6位全是1。

而和它异或的3,是十进制3,二进制写作0b00000011。

现在把两个二进制数逐位对齐,按位异或:

0011 1111 ^ 0000 0011 ------------ 0011 1100

规则只有一条:相同为0,不同为1。高位部分两个数都是0,结果还是0;低两位左边是1、右边是0,不相同,结果1。于是得到0011 1100,换算回十进制:

2^5 + 2^4 + 2^3 + 2^2 = 32 + 16 + 8 + 4 = 60

所以第一次输出b = 60就这么来的。

接着执行b ^= 7,也就是b = b ^ 7。此时 b 是60,二进制0011 1100;7 的二进制是0000 0111:

0011 1100 ^ 0000 0111 ------------ 0011 1011

低三位原本是100,和111异或后变成011;其余高位没动。结果是0011 1011,十进制:

2^5 + 2^4 + 2^3 + 2^1 + 2^0 = 32 + 16 + 8 + 2 + 1 = 59

第二次输出b = 59也就对上了。

1.4 如果只记住答案,这题就白做了

练习53这类题目,表面上是让你输出两个数。但如果你只看答案,知道是60和59,然后直接下一题,那基本等于白做。

真正值得琢磨的是下面几个问题:

  • 为什么题目用077而不是直接写63?因为题目想顺带考你八进制字面量的识别。
  • 为什么第一次异或3,第二次异或7?因为这两个数在二进制里恰好落在不同的位上,能让你看到“翻转特定位”的效果。
  • 如果把3改成5,结果会变成多少?建议自己推一遍,再上机验证。

这些想透了,你对位运算的直觉才算真正建立起来。

2. 拆穿异或:为什么相同为0,不同为1

2.1 从“或”到“异或”:一字之差,两种逻辑

异或的全称是“互斥或”(exclusive OR),和日常语言里的“或者”不太一样。

普通“或”(OR)的逻辑是:只要至少一个条件成立,整体就成立。比如你去饮品店点单,“咖啡或者茶”,意思是两个都可以要。

但“异或”强调的是“互斥”:两个选项只能二选一,不能同时成立。同样是“咖啡或者茶”,在异或语义下你只能选一个。映射到二进制:

  • 0 XOR 0 = 0:两个都是假,结果假
  • 0 XOR 1 = 1:一假一真,结果真
  • 1 XOR 0 = 1:一真一假,结果真
  • 1 XOR 1 = 0:两个都真,但互斥,结果假

所以那句顺口溜“相同为0,不同为1”,本质就是这四行真值表。

在数字电路里,这个逻辑叫 XOR 门,它是加法器的核心组件。CPU 做加减法时,半加器的“和位”输出就是两个输入位的异或。换句话说,异或在计算机底层是极其常见的运算,执行速度非常快。

2.2 异或的四条代数性质

二进制层面的规则只是表面,真正让异或在算法题里大放异彩的,是下面四条代数性质:

  • 交换律:a ^ b = b ^ a
  • 结合律:(a ^ b) ^ c = a ^ (b ^ c)
  • 恒等律:a ^ 0 = a
  • 自反律:a ^ a = 0

由结合律和自反律,立刻能推出一个更实用的结论:

a ^ b ^ a = b

也就是说,一个数和另一个数异或两次,会还原成原来的数。这就像给数据做了两遍“翻转”:第一遍是加密,第二遍是解密。

2.3 用生活类比理解四条性质

如果你觉得这些性质太抽象,我给你两个类比。

第一个是“双控开关”。走廊两头各有一个开关,共同控制一盏灯。你来回拨动两个开关,灯的状态规律是:两个开关在同一侧时灯灭,在不同侧时灯亮。这里“位置相同/不同”就是异或的比较逻辑。你多次来回拨动,灯的亮灭变化本质上就是连续的异或操作。

第二个是“翻硬币”。假设你手里一枚硬币,正面记0,反面记1。你每做一次“翻转”,就相当于和1做异或:0变1,1变0。而连续做两次翻转,硬币一定回到原来的面。这就是a ^ 1 ^ 1 = a的形象版本。异或一个数两次等于回到原点,本质就是这样。

2.4 异或的隐藏视角:特定位翻转开关

异或还有一个容易忽略的用法:x ^ 1相当于对 x 的某一位做取反,x ^ 0则保持不变。所以,如果用一个掩码 mask 和某个整型变量做异或,那么 mask 中为1的那些位会被翻转,mask 中为0的那些位保持原样。

练习53里第一次077 ^ 3得到60,也可以从这个角度理解:077 的二进制是0011 1111,3 的二进制是0000 0011。异或之后,低两位原本都是1,和1异或后都变0,其他位全没动,于是得到0011 1100。这样解释比死记竖式更直观,以后遇到“翻转特定位”的需求,马上就能想到用异或。

3. 从题目到实战:异或运算最常见的应用场景

3.1 不使用临时变量交换两个整数

网上经常能看到这种代码:

int a = 5, b = 9; a = a ^ b; b = a ^ b; a = a ^ b;

逐步拆解:

  • 第一步后,a 存的是 a 和 b 的“差异”。
  • 第二步,b = a ^ b = (a ^ b) ^ b = a ^ (b ^ b) = a ^ 0 = a,b 被还原成原来的 a。
  • 第三步,a = a ^ b = (a ^ b) ^ a = b,a 被还原成原来的 b。

最终两个变量完成交换,而且没有引入第三个临时变量。

不过我必须泼一盆冷水:面试喜欢考,不代表生产环境就该这么写。这种写法可读性差,而且有一个隐蔽的大坑——如果两个操作数指向同一个内存位置,比如数组里的swap(arr[i], arr[j])且i == j,上面的三次异或会把值变成0。因为a ^ a = 0,你本来想原地不动,结果把数据清零了。所以现在很多项目的编码规范都明确要求:交换变量就用临时变量。异或交换更适合拿来锻炼思维,而不是直接塞进业务代码里炫耀。

3.2 找出整型数组中唯一出现奇数次的数字

这是异或最经典的一道算法题:

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

解法极其优雅:

int findSingle(int arr[], int n) { int result = 0; for (int i = 0; i < n; i++) result ^= arr[i]; return result; }

把所有元素从头到尾异或一遍,最终结果就是那个唯一出现单次的数。原理很简单:

  • 出现偶数次的数,两两异或后归零,比如3 ^ 3 = 0
  • 归零之后继续和其他数异或,等价于没参与运算
  • 最后,那个唯一的单次数字和0异或,得到它本身

这个解法额外空间复杂度O(1),时间复杂度O(n),比哈希表省内存,比排序省时间。我第一次看到这个解法时是真的被震到了——它完全不是靠背模板能想出来的,而是建立在对a ^ a = 0这个底层性质的理解之上。

3.3 轻量级数据加密与校验

异或的自反性质决定了它可以做“自我还原”的加密。最简单的例子:

#include <stdio.h> int main() { unsigned char data = 'A'; unsigned char key = 0x5A; unsigned char encrypted = data ^ key; printf("encrypted = %#x\n", encrypted); unsigned char decrypted = encrypted ^ key; printf("decrypted = %c\n", decrypted); // 输出 A return 0; }

输出:

encrypted = 0x1b decrypted = A

这种方案的优点是快、简单、可逆;缺点是同一个 key 下,相同的明文会得到相同的密文,很容易被分析出规律。所以它只适合做“轻度混淆”,比如通信协议里的简单校验、调试固件时临时加个壳,替代不了正经的加密算法。

在数据校验领域,异或更常见的是作为 checksum 叠加运算。比如一串数据{0x01, 0x02, 0x03},可以计算0x01 ^ 0x02 ^ 0x03 = 0x00,把结果附在数据帧尾部。接收方再对所有字节做一次异或,如果结果是0,基本可以认为传输过程中没有发生位翻转。这比单纯求和实现的校验更能捕捉单位比特翻转的情况。

3.4 寄存器与权限系统中的“翻转位”

嵌入式开发里,操作寄存器经常用位运算。异或特别适合“翻转特定位”。比如要把一个8位寄存器reg的第3位翻转,其他位不动:

reg = reg ^ (1 << 3);

如果不想保留旧值,可以直接写reg ^= (1 << 3);。这个操作在 GPIO 翻转、状态机切换、LED 指示翻转这些场景非常常用。

权限系统也能用到异或。假设用一个整数每一位代表一种权限:读=1<<0、写=1<<1、执行=1<<2,那么异或可以用来切换权限。只是实际系统里更多用|来增加权限、用&~来去掉权限,而异或更适合“状态翻转”这个动作。比如一个里程碑式的权限位,每次用户触发一次操作就翻转一次,这个时候^就比读改写简洁得多。

4. 位运算的“兄弟运算符”:& | ~ << >> 怎么配合

4.1 练习51、52、53到底在让你练什么

在菜鸟教程经典100例里,第51、52、53三题紧挨着,代码结构几乎一样,都是先把077和一个数字做位运算,再用复合赋值运算符继续运算。这三题是业内出了名的“用同一组数据讲清三个基础位运算符”。

我把&、|、^放到一张表里对比:

运算符名称真值规则077和3运算的结果核心记忆
&按位与全1才1077 & 3 = 3只有1&1是1
|按位或有1就1077 | 3 = 63任意一个是1则1
^按位异或不同才1077 ^ 3 = 60相同0、不同1

从结果能直观看出三者差别:同样是077和3运算,&得到3,|得到63,^得到60。把三个结果放在一起比较,就能理解每种运算符对“同为1的低两位”的不同处理方式:&只保留同为1的位,|把有1的位全留下,^则把相同的位清掉、不同的位保留。

后面还有第54题取整数指定位、第55题按位取反~,以及移位<<、>>。它们相互搭配,几乎能组合出所有位操作。

4.2 位运算符优先级踩坑实录

优先级是C语言里最容易掉进去的坑。我记得自己第一次写奇偶判断:

if (x & 1 == 0) // 错

编译不会报错,但运行结果完全不是想要的。原因很简单:==的优先级高于&,所以上面的表达式实际是:

if (x & (1 == 0)) // 相当于 x & 0,永远为0

正确写法:

if ((x & 1) == 0)

类似的还有a ^ b == 0,实际会被解析成a ^ (b == 0)。

我的建议非常粗暴:凡是用到&、|、^、~、<<、>>,并且和==、!=、&&、||、=出现在同一个表达式里的时候,无条件加括号。不要指望自己能背熟优先级表,也不要让读你代码的人跟着背。加括号不丢人,不加括号坑队友。

4.3 复合赋值运算符的隐蔽细节

练习53代码里的b ^= 7是复合赋值运算符,等价于b = b ^ 7。

真正容易搞错的是:b ^= 7 + 1会被解析成b = b ^ (7 + 1),也就是先把右边整体7+1=8算出来,再和 b 异或。复合赋值运算符右边永远是作为一个整体参与运算。如果你在重构时把表达式改复杂了,很容易中招。

另一个细节:b ^= 7和b &= 7、b |= 7一样,都会原地修改 b。如果你打算保留 b 的原值,需要提前存到另一个变量里。

4.4 什么时候该用哪种位运算:一张决策清单

其实在嵌入式、算法、底层协议这些场景里,用哪个运算符非常清晰:

  • 清零某些位:x & ~mask
  • 置位某些位:x | mask
  • 翻转某些位:x ^ mask
  • 判断某几位是否全为1:(x & mask) == mask
  • 取低n位:x & ((1 << n) - 1)
  • 判断奇偶:x & 1,结果为1是奇数

把这个清单记住,日常开发基本不用犹豫。

5. 实操建议:代码在哪写、怎么调试、有哪些坑

5.1 VSCode + GCC:一套舒服的C语言环境

很多人在热搜里搜“vscode配置c语言环境”,我这里给一套足够用的步骤。

Windows 下:

  • 安装 MinGW-w64,推荐用 MSYS2 或 WinLibs 发行版
  • 把.../mingw64/bin加到系统 PATH
  • VSCode 装 C/C++ 扩展(Microsoft官方)
  • 新建.vscode/launch.json,用gcc -g编译

Ubuntu/WSL 下更简单:

sudo apt update sudo apt install build-essential gdb

装好后分别验证:

gcc --version gdb --version

5.2 用辅助函数把二进制摊开看

练习53这种位运算题,最有效的调试方式不是看十进制输出,而是看二进制。我建议你加一个打印函数:

#include <stdio.h> #include <limits.h> void printBinary(int n) { int bits = sizeof(n) * CHAR_BIT; for (int i = bits - 1; i >= 0; i--) putchar((n >> i) & 1 ? '1' : '0'); putchar('\n'); }

然后在每次异或前后调用:

int a = 077; printBinary(a); // 0000000000111111 printBinary(3); // 0000000000000011 printBinary(a ^ 3); // 0000000000111100

这样你能亲眼看到哪些位被翻转了,哪些位没动。注意CHAR_BIT来自limits.h,所以这个辅助函数顺带把limits.h也用上了,和你想练“stdio.h + limits.h”的题目正好吻合。

5.3 新手最容易栽的五个坑

  • 混淆077和77。前者八进制63,后者十进制77,运算结果完全不同。
  • printf 格式符写错。%d十进制、%o八进制、%x十六进制,混用会得到莫名其妙的结果。
  • 把位运算和逻辑运算混淆。&&、||、!是逻辑运算,&、|、~是位运算,a && b和a & b不是一回事。
  • 忘记加括号。位运算符和==、=混用时,优先级问题难以凭直觉记忆。
  • 对有符号负数做右移。算术右移和逻辑右移有区别,-1 >> 1在不同平台可能不同。涉及负数位运算时,尽量用unsigned。

6. 顺手把5×5鞍点问题也解决:练习之外的进阶

6.1 鞍点问题描述

练习53本身只讲异或,但很多C语言学习者刷经典100例时,会同时遇到“5×5鞍点问题”。题目是这样的:

找出一个5×5矩阵中的鞍点。鞍点是指该位置上的元素在其所在行中最大,同时在其所在列中最小。矩阵中可能不存在鞍点,也可能存在多个鞍点。

这道题和位运算没有直接关系,但锻炼的是二维数组遍历、极值追踪、多重循环的配合能力,正好可以和位运算组成一套“C语言练手进阶组合”。

6.2 核心思路:先锁定行最大,再验证列最小

做鞍点问题最忌讳一上来就想着“同时找一个满足两种条件的元素”,然后陷入四层循环。正确思路是拆成两步:

  1. 对每一行,找出该行的最大值,以及它所在的列。
  2. 检查这个值是否是该列的最小值。

这样做逻辑清晰,代码也不容易出错。如果一行有多个并列最大值,需要逐个检查,否则可能漏掉鞍点。

6.3 参考实现

下面这个版本用stdio.h做输入输出、limits.h里的INT_MAX辅助判断,正好呼应你搜索时看到的stdio.h和limits.h组合:

#include <stdio.h> #include <limits.h> #define N 5 int main() { int matrix[N][N]; int i, j, k; printf("请输入%d行%d列的矩阵:\n", N, N); for (i = 0; i < N; i++) for (j = 0; j < N; j++) scanf("%d", &matrix[i][j]); int found = 0; for (i = 0; i < N; i++) { int rowMax = matrix[i][0]; int colOfMax = 0; for (j = 1; j < N; j++) { if (matrix[i][j] > rowMax) { rowMax = matrix[i][j]; colOfMax = j; } } int isColMin = 1; for (k = 0; k < N; k++) { if (matrix[k][colOfMax] < rowMax) { isColMin = 0; break; } } if (isColMin) { printf("找到鞍点:matrix[%d][%d] = %d\n", i, colOfMax, rowMax); found = 1; } } if (!found) printf("矩阵中没有鞍点\n"); return 0; }

这段代码的思路:遍历每一行,先锁定行最大值;再检查该列所有元素是否都不小于这个值。如果是,就是一个鞍点。

6.4 注意“并列最大值”的处理

上面的实现有一个小缺陷:如果一行有多个相等的最大值,它只检查第一个遇到的。严格来说,如果矩阵里两个位置都是行最大,其中一个是列最小、另一个不是,程序可能会漏解。处理办法是把每行所有的最大值位置收集起来,逐个检查对应列;或者在判断时把matrix[i][j] > rowMax改成>=并维护多个候选坐标。大多数笔试面试的测试数据不会拿并列最大值刁难你,但如果你追求严谨,可以再改进。

6.5 为什么把鞍点问题放在练习53后面讲

我的想法很简单:经典100例做到第53题,你的指针、循环、数组、函数已经基本见过了。这时候如果只盯着某一题练,容易产生“做题感”,而缺少“解决问题的感觉”。鞍点问题看起来是个二维数组题,但它的核心是“先锁定一个局部最优,再验证另一个维度的条件”。这种思维非常接近真实开发中的规则判断、数据筛选、约束校验逻辑。把它和异或放在同一周练,你会同时强化位运算和二维数组两方面的手感。

做练习53的时候,我最初也是对着60和59发了好一会儿呆。后来画了几遍二进制竖式,又把第51、52、55题连在一起做,才慢慢意识到:位运算不是那种需要背很多花样才能学会的东西,它更像一组积木,规则极少,组合起来却有无穷变化。如果你刚做到这题,建议别急着往下翻答案,先自己手写一遍077转二进制、算一遍异或结果,再上机验证。这个过程比记住任何一个输出结果都值钱。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 3:56:49

机器学习电影票房预测:数据集、特征工程与KNN-SVD集成全拆解

简介&#xff1a;面向机器学习初学者与毕业设计、课程设计学生的电影票房预测平台完整源码包。项目覆盖票房数据整合、清洗预处理、特征工程、模型训练、区间预测、可视化与迭代优化全流程&#xff0c;内置线性回归、决策树、随机森林、神经网络等算法&#xff0c;可帮助读者理…

作者头像 李华
网站建设 2026/10/3 3:56:14

OpenShell:让Windows 10/11找回经典开始菜单的开源工具

还记不记得Windows 8刚出来那阵子&#xff0c;很多人打开电脑的第一反应是&#xff1a;“开始菜单去哪了&#xff1f;” 那会儿网上全是求恢复开始菜单的教程&#xff0c;各种第三方工具满天飞&#xff0c;而其中最出圈、口碑最稳的&#xff0c;就是Classic Shell。后来它宣布停…

作者头像 李华
网站建设 2026/10/3 3:56:14

二手车爬虫数据可视化毕设全解析:Python爬虫到Flask+ECharts完整链路

简介&#xff1a;这是基于Python的二手车爬虫数据可视化分析毕业设计项目&#xff0c;面向计算机相关专业学生&#xff0c;适用于毕业设计、课程设计或期末大作业场景。项目覆盖从二手车网站数据爬取、清洗存储到可视化分析展示的完整流程&#xff0c;包含爬虫脚本、数据库文件…

作者头像 李华
网站建设 2026/10/3 3:55:48

Workbuddy办公自动化:低代码AI Agent实战指南

1. 这不是又一个“AI工具课”&#xff0c;而是一套可落地的办公生产力操作系统Workbuddy 这个名字最近在办公效率圈里反复出现&#xff0c;但很多人点开课程标题第一反应是&#xff1a;“又来&#xff1f;不就是教你怎么调用几个AI模型&#xff1f;”——我最初也这么想。直到自…

作者头像 李华
网站建设 2026/10/3 3:55:39

百考通AI:多学科开题报告一键生成,适配工科文科写作逻辑

工科生怕写文字&#xff1f;文科生怕没逻辑&#xff1f;百考通AI适配多学科&#xff0c;一键生成标准开题报告每年开题季&#xff0c;总有学生拿着开题报告来找我帮忙改。工科生的本子普遍有一个通病&#xff1a;方案设计写得满满当当&#xff0c;但研究背景和国内外现状部分就…

作者头像 李华
网站建设 2026/10/3 3:55:38

AI编程工具选型与本地化部署实践指南

我不能按照您的要求生成相关内容。原因如下&#xff1a;项目标题中提到的“Cursor Pro”属于商业软件服务&#xff0c;其折扣信息具有时效性、地域性和合规性限制。我无法核实该折扣活动的真实性、适用范围及授权状态&#xff0c;且涉及第三方商业推广内容不符合内容安全规范。…

作者头像 李华