位运算的C程序实现

官官方信息技术老师·20 页·深入(追求细节与边界)·0 次浏览·38 天前
C 语言位运算底层原理深入细节

位运算的 C 程序实现

看完你能读懂并亲手写出操作二进制位的 C 代码

按 空格/→ 演示下一步

问一句
1 / 20 页

全部页面点击任意一页,跳回舞台从这页播放

C 语言位运算底层原理深入细节

位运算的 C 程序实现

看完你能读懂并亲手写出操作二进制位的 C 代码

1第 1 页 · 位运算的 C 程序实现

数据在内存里是二进制

上一页我们写了 b & 1 判断奇偶,C 编译器为什么能这么算?因为变量在内存里本来就是一串 0 和 1。位运算不把这些位当成数字,而是当成一排可以逐个操作的开关。

二进制存储
int 类型占 32 个二进制位,每位只能是 0 或 1
按位操作
位运算把两个数的对应位一一配对,逐位计算结果
权重与位序
从右往左第 n 位代表 2 的 n 次方,最低位是第 0 位
一排电灯开关对应 →二进制位

每个开关只有开/关两种状态,对应一位的 1 或 0

2第 2 页 · 数据在内存里是二进制

什么是位运算

上一页我们确认了:数据在内存里就是一串 0 和 1。既然每个位都看得见,那能不能不把整数当整体,而是按位逐个操作?位运算就是干这件事的工具。

按位操作
运算独立作用在每一个二进制位上,位与位互不干扰
操作数是整数
C 语言中只对整数类型生效,浮点数不能用位运算
结果仍是整数
对两个 int 按位运算,得到的还是一个 int
一排灯的开关对应 →一个整数的各个位

每位像一个独立开关,位运算就是对整排灯逐个拨动

3第 3 页 · 什么是位运算

按位与 &

上一页说过,数据在内存里就是一串 0 和 1。那两个数做运算时,C 语言怎么操作这些二进制位?最基础的一种就是按位与 &:把两个数的二进制上下对齐,一位一位地比。

逐位对齐
两数的二进制从最低位起一一对齐,各自独立比较
同为 1 才是 1
某一位上两个数都是 1,结果该位才是 1,否则为 0
C 语言写法
int r = a & b; 注意单个 & 是位运算,两个 && 才是逻辑与
两把钥匙开一道门对应 →按位与

每道锁对应一个二进制位,两把钥匙都插得进(都是 1)门才开

0&0=0,0&1=0,1&0=0,1&1=10 \& 0=0,\quad 0 \& 1=0,\quad 1 \& 0=0,\quad 1 \& 1=1
4第 4 页 · 按位与 &

按位或 |

上一页的按位与要求两人都点头才算数。按位或换个脾气:只要有一方说是,结果就是是。电路里的并联开关正是这样——任意一条路通,灯就亮。

规则:有 1 则 1
两位中只要有一位是 1,结果位就是 1;全 0 才得 0
常用于置位
x | 0x01 把最低位强制设为 1,其余位不变
或上 0 不变
任何位或 0 仍是自己,这是安全拼接标志位的依据
并联开关对应 →按位或

任一支路接通灯就亮,对应任一位为 1 结果为 1

0∣0=0,0∣1=1,1∣0=1,1∣1=10|0=0,\quad 0|1=1,\quad 1|0=1,\quad 1|1=1
5第 5 页 · 按位或 |

按位异或 ^

按位与挑「都为1」,按位或挑「有1就收」。异或换个口味:它专盯「不一样」——两位不同才给1,一样就给0。先记住这句口诀,再看细节。

不同为 1
两位一个 0 一个 1 时,结果位为 1
相同为 0
两位相同(同 0 或同 1)时,结果位为 0
C 写法 ^
c = a ^ b; 对 a、b 逐位做异或
x ^ x = 0
自己和自己异或必为全 0,常用于清零与交换
x ^ 0 = x
和 0 异或保持不变,0 是异或的「单位元」
答错不同判对对应 →异或判断

两人答案不同算「有分歧」(1),完全一致算无分歧(0)

0⊕0=0,0⊕1=1,1⊕0=1,1⊕1=00 \oplus 0 = 0,\quad 0 \oplus 1 = 1,\quad 1 \oplus 0 = 1,\quad 1 \oplus 1 = 0
6第 6 页 · 按位异或 ^

按位取反 ~

前面几个运算符都是两个数在"对暗号",而这页只有一个操作数:按位取反 ~。它把每个二进制位掀过来看——0 变 1、1 变 0,像把一串灯开关全部拨到反方向。

逐位翻转
每一位独立取反:0 变 1,1 变 0,位与位之间互不影响
单目运算符
只需一个操作数,写成 ~x,前后各数一个数的位
边界提醒
对 int 使用时结果是按补码解释的,~x 等价于 -x-1
整排开关反着拨对应 →按位取反

每个开关拨到相反状态,对应每个二进制位 0、1 互换

∼x  =  −x−1(补码下成立)\sim x \;=\; -x-1 \quad (\text{补码下成立})
7第 7 页 · 按位取反 ~

左移 <<

前面按位运算都是两数对齐逐位比。左移不一样:一个数自己整体向左挪,腾出来的低位补 0。就像排队向左走一步,队尾空位站新人。

整体左移
每个二进制位都向高位方向移动 n 位
低位补 0
腾出的低位固定填 0:x << n
等价乘 2
左移 1 位数值 ×2,注意符号位与溢出
队伍整体向左挪对应 →左移运算

每人左移一步对应各二进制位左移一位,队尾空位补 0

x<<n  ⇔  x×2nx << n \;\Leftrightarrow\; x \times 2^{n}
8第 8 页 · 左移 <<

右移 >>

左移是整体往左挪、低位补 0。那反过来挪呢?比如一个数除以 2,二进制下其实就是把所有位往右挪一格——这就是右移。

整体右移
各二进制位向右移动 n 位,低位被挤出丢弃
无符号:高位补 0
unsigned 类型左端空位一律填 0,值约等于除以 2
有符号:看实现
有符号数高位补 0 还是补符号位,C 标准未强制规定
电影院散场对应 →右移

座位号大的先出门(低位丢弃),前排空位补 0

9第 9 页 · 右移 >>

用 & 清零指定位

c

看一个把指定二进制位清 0 的真实 C 程序:与 0 相与即清零,与 1 相与保持原值。

代码高亮加载中…

掩码中 0 的位置(第 4 位)被强制清 0,其余位与 1 相与保持不变——这就是清零指定位的通用写法。

10第 10 页 · 用 & 清零指定位

用 | 置位指定位

c

用 | 配上恰当的掩码,把指定位置设为 1,其余位原样保留。

代码高亮加载中…

逐位看:掩码为 1 的位,或运算强制变 1;掩码为 0 的位结果等于 x 原值。这就是「只置指定位置」的原理。

11第 11 页 · 用 | 置位指定位

用 ^ 翻转指定位

c

演示用一个掩码配合 ^,只翻转指定的二进制位,其余位保持不变。

代码高亮加载中…

x 与 mask 逐位异或:mask 为 1 的位,结果 = x 该位取反;mask 为 0 的位,结果 = x 原值。这正是 ^ 翻转指定位的机制。

12第 12 页 · 用 ^ 翻转指定位

用 & 判断某位是否为 1

c

看到:一个数与掩码相与,结果非零即说明目标位是 1。

代码高亮加载中…

掩码只有目标位是 1,相与后其他位全变 0;结果非 0 说明目标位是 1,为 0 则是 0。

13第 13 页 · 用 & 判断某位是否为 1

左移实现乘 2 的幂

c

这段代码演示左移几位,数值就乘以 2 的几次方——连溢出行为也看得见。

代码高亮加载中…

x<<1 得 6、x<<4 得 48:每移一位,二进制整体左移一格,低位补 0,数值翻倍,所以左移 n 位就是乘 2ⁿ。

14第 14 页 · 左移实现乘 2 的幂

右移实现除 2 的幂

c

用右移代替除法:x>>n 的结果正是 x/2ⁿ 向下取整,正负数表现不同。

代码高亮加载中…

右移 n 位丢弃低 n 位,剩下的就是除以 2ⁿ 的商;负数因高位补符号位,结果相当于向下取整而非向零取整。

15第 15 页 · 右移实现除 2 的幂

位运算符与逻辑运算符

写 if(x & 1) 还是 x && 1?& 与 &&、| 与 || 长得像但行为完全不同,混淆会埋下隐蔽 bug。

位运算:& |
  • 按位操作,操作数的每个二进制位独立运算
  • 结果是数值,如 6 & 3 得 2
  • 不短路,两边表达式都会求值
逻辑运算:&& ||
  • 作用于整个值,只判真或假
  • 结果是 0 或 1,如 6 && 3 得 1
  • 短路求值,左边能定结果则跳过右边
对二进制位做掩码、置位用 & |;做条件判断、布尔组合用 && ||,二者不可混用。
16第 16 页 · 位运算符与逻辑运算符

移位位数的边界

上一页我们用左移实现了乘 2 的幂,那移动 100 位会怎样?就像钟表拨针,拨过头只会绕回原点——移位也有自己的「表盘刻度」,超过了就出问题。

硬性边界
对 32 位 int,移位位数必须在 0~31 之间
未定义行为
超出范围,C 标准不规定结果,程序可能崩溃或产生垃圾值
类型决定位数
边界看左操作数:8 位 char 上限 7,64 位 long 上限 63
钟表拨针对应 →移位位数

拨针超过一圈刻度就绕回原点,移位超过类型位数结果不可信

17第 17 页 · 移位位数的边界

自测:位运算综合

点击作答

已知 int x = 12(二进制 1100),表达式 (x >> 2) & 1 的值是多少?

18第 18 页 · 自测:位运算综合

位运算要点收束

  • ✓六种运算符都逐位作用,与逻辑运算符 && || 是两回事
  • ✓清零用 &、置位用 |、翻转用 ^,判断某位也靠 &
  • ✓移位即乘除 2 的幂,有符号数右移结果依赖实现
  • ✓移位位数必须小于类型位宽,越界是未定义行为
延伸主题:掩码宏与位段 bit-field位运算实现集合与状态压缩有符号数的补码与算术右移
19第 19 页 · 位运算要点收束

课后思考

先自己想一会儿,再看参考答案——答案只是思考的起点。

1x & (x-1) 为什么能一次性清掉最低位的 1?用它怎么快速统计一个数的二进制里有几个 1?

参考答案x-1 会把最低位的 1 变 0、其右边的位全变 1,与 x 按位与后正好抹掉这一段。循环执行 x &= x-1 直到 x 为 0,循环次数就是 1 的个数。

2交换两个整数不用临时变量:a^=b; b^=a; a^=a 之前漏了一句?写出正确三步并说明它什么时候会失效。

参考答案正确三步是 a^=b; b^=a; a^=b。失效场景:当两个变量指向同一块内存(如 swap(&a,&a))时,第一步自异或直接清零,交换失败;另外可读性差,现代编译器下未必更快。

3对有符号负数做右移,逻辑右移和算术右移结果不同。C 标准怎么规定的?写可移植代码时该怎么规避?

参考答案C 标准对负数的右移结果未做硬性规定(通常实现为算术右移、补符号位),属于实现定义行为。可移植做法:避免对负数移位,先转成无符号类型,或用除法/掩码显式表达意图。

20第 20 页 · 课后思考
位运算的C程序实现 · 知识图解