欢迎光临
我们一直在努力

& | ~ ^ << >> 一堆符号,到底怎么记?

不就是与或非吗,能难到哪去?

今天开始学位运算,说实话,翻开笔记之前我心里想的是:与或非,不就是逻辑运算那一套嘛,true/false 换成了 1/0 而已,有什么好学的。结果看到第一个例子就愣住了——不是判断真假,是直接把两个十六进制数按位对齐,一位一位地算。0x55 & 0x33,算出来是 0x11。我停下来对着二进制数了半分钟,才反应过来:原来这里的“与”,不是“条件 A && 条件 B”,是每个二进制位各自做与。这是两个完全不同的东西。

后面再看到“清 0 用与,置 1 用或”的时候,我已经不敢大意了——你以为你懂了,其实你只懂了字面意思。

“清 0”和“置 1”,到底在干什么?

学到这里我忍不住想了一个问题:为什么要费这么大劲去动某一个二进制位?直接重新赋个值不行吗?

然后翻到 51 单片机那部分,看到 P1 口那 8 个引脚,每个引脚对应寄存器里一位,0 是低电平,1 是高电平。你要控制其中一个引脚输出高电平,不能把整个寄存器都覆盖掉——旁边那 7 个引脚还在干着别的活呢。这时候就理解为什么需要“只改某一位,别的位不能动”了。

与运算用来清 0 的逻辑是这样的:想让某一位变 0,就让那一位跟 0 做与。1&0=0,0&0=0,不管原来是啥,结果都是 0。其他位呢,跟 1 做与,原来是什么还是什么。所以清 0 的口诀就是“拿 0 去与”。

// 想把 a 的第 10 位清零,其他位保持不动
int a = 0xffffffff;
a = a & ~(1 << 10); // 1<<10 得到一个只有第 10 位是 1 的数
// ~ 取反之后,只有第 10 位是 0,其余全是 1
// 跟 a 做与:第 10 位必然变 0,其他位原样保留

或运算置 1 是一个道理,想让某一位变 1,就拿 1 去或。1|0=1,1|1=1,目标位不管原来是啥都给你变成 1。其他位跟 0 去或,原来是啥还是啥。

所以清 0 用与、置 1 用或,背后其实都是同一个逻辑:不动的位用"中性值"罩住(与是 1,或是 0),要改的位用"目标值"去怼。

我当时觉得这俩挺对称的挺好记,结果后面一翻,看到一行代码直接把我看傻了:

a = a & ~(1<<10 | 1<<12 | 1<<13);

一行干掉三个位。我才意识到之前学的那些“清 0 置 1”只是基础动作,实际写代码的时候都是多个位一起处理的。先把要改的位拼出来,再一次性操作。

左移右移,原来不是“往左挪一下”就完了

左移还算好理解:a<<n 就是 a 乘以 2 的 n 次方。我在纸上画了一下二进制,1 左移 1 位变成 2,再移一位变成 4,确实就是乘 2。当时还想,这比直接写乘法快吗?后来查了一下,位运算确实比乘除法快很多,在嵌入式这种资源紧张的地方是有实际意义的。

右移就坑了。我开始以为左移乘 2,右移就是除 2 呗——确实也对,但笔记里突然冒出两个词:逻辑右移和算术右移。我当时愣了一下,右移不都一样吗?

区别在最高位。如果 a 是无符号类型,右移之后最高位补 0,这叫逻辑右移。如果 a 是有符号类型,右移之后最高位补的是原来的符号位,这叫算术右移。举个例子:-8 用补码表示,右移一位如果是逻辑右移,最高位补 0,就变成正数了,这显然不对。算术右移补符号位,负数还是负数。

这个点我琢磨了好一会儿:平时写代码不太留意有符号无符号,但碰到右移的时候,类型选错了结果能差出十万八千里。

异或,看起来最不起眼,结果花样最多

异或的规则就八个字:相同为 0,不同为 1。我当时扫了一眼就想翻页,结果被两道题摁住了。

第一道:用异或交换两个数,不用第三个变量。

int a = 0x55, b = 0x33;
a = a ^ b; // 此时 a 变成了 a^b,b 还是原来的 b
b = a ^ b; // b = (a^b)^b = a,b 拿到了 a 原来的值
a = a ^ b; // a = (a^b)^a = b,a 拿到了 b 原来的值
// 就这么三行,两个数交换完了,不用临时变量

我在纸上把每一步按位推了一遍,推到第三行的时候突然就觉得这个东西挺妙的——利用了“一个数异或自己得 0,异或 0 得自己”这两个性质,绕了一圈就把值换过来了。

第二道更有意思:位运算还能做加密。明文用某个密钥异或一次得到密文,密文再用同一个密钥异或一次,明文就回来了。笔记里演示的就是这个效果:0x55 异或 0x33 得到 0x66,0x66 再异或 0x33,又变回 0x55。

学到这儿我是真觉得位运算不像一开始以为的那么无聊了——跟之前学的那些基础语法完全不是一个感觉。

两个小练习逼出了“组合拳”思维

这种"多个位一起操作"的思路,后面两个练习题里就用上了。

笔记后面给了一个面试题:统计一个 int 里有多少个 1。我的第一反应是一个一个数,但一个 int 有 32 位,循环 32 次也太蠢了。笔记里给的思路是“跟 1 做与,判断最低位是不是 1,然后右移一位继续”——我觉得这是最直观的方法,虽然肯定有更高效的算法,但目前先把这个搞懂就够了。

还有一个“循环左移”的题目,大概思路是:先看最高位是 0 还是 1,然后左移腾出最低位,如果刚才最高位是 1 就把最低位或成 1。这个我还没自己敲,但大概方向理解了——就是用 & 判断,用 | 设置,用 << 移动,几个符号打配合。

其实学到位运算这里,我感觉它不像一个独立的知识点,更像一种底层编程的手感——你要时刻意识到数据在内存里是一串二进制,你要做的就是精确控制这串二进制里的某几位。这种感觉在之前学变量、循环、数组的时候从来没有过。

情况为什么容易错
把 & 当成 &&,把 | 当成 || 脑子里还停留在逻辑运算模式,忘记这里是一个一个位单独算
右移时忘了区分有符号和无符号 平时写代码不关注最高位补什么,但右移的时候补 0 还是补符号位结果完全不同
清 0 用 |,置 1 用 & 两者操作逻辑相反,刚学的时候非常容易记反,写反了该清的没清、该置的没置
以为 ~ 取反只是把 0 变 1、1 变 0 就完了 忽略了 ~ 是按位取反整个数,包括高位全变,配合 & 用的时候容易算错范围
异或交换两个数以为可以随便用 浮点数不能做位运算,用异或交换也只适用于整型,而且两个数指向同一块内存的时候会出 bug
左移以为就是简单往左挪 左移超过类型宽度是未定义行为,而且左移一位是乘 2 没错,但不能无脑替代乘法

今天学完位运算,脑子有点发热。从几个小符号开始,一路跑到操作寄存器、控制硬件、甚至看了 2048 的项目框架,信息量大得有点撑。不过我很确定一点:位运算不是一个背规则的东西,得在纸上多画几遍二进制,真的把每一位的变化看清楚,心里才踏实。今天没来得及看 2048 的具体实现,明天得把合并和移动的算法跑一遍——刚好顺便把 Day15 的结构体和数组用上。

Day16 学习记录,明天继续。

赞(0)
未经允许不得转载:171主机测评 » & | ~ ^ << >> 一堆符号,到底怎么记?
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址