🔥 星光编译者 · 个人主页
📚 学习专栏: 《C/C++ 成长笔记》 · 《Linux 实践手册》 · 《数据结构与算法》
🌄 向云端飞扬,编译属于自己的代码星河。
☕ 写在开篇
你好,这里是 星光编译者。
这里记录我在 C/C++、Linux、数据结构与算法 学习中遇到的真实问题、亲手验证过的代码,以及那些容易被忽略的实现细节。
比起简单罗列结论,我更愿意从问题出发,把一个知识点的来由讲清楚,把“为什么会这样”和“应该怎样解决”说明白,让每一次踩坑都沉淀成可以复用的经验。
如果这篇记录能帮你少绕一点路,或让某个模糊的地方忽然变得清晰,那么这次分享便有了意义。愿我们在一次次阅读、编译与调试中稳步向前,慢慢搭起属于自己的技术世界。
🔥 本文定位:从一串二进制到底表示什么开始,逐步读懂 <<、>>、&、|、^、~,最后用统计二进制 1 的个数和按位置位练手。
💡 学习目标:会做二、八、十、十六进制转换;理解常见补码模型与 C 标准的边界;能安全地写无符号移位和位掩码;知道为什么某些“看起来能运行”的位运算代码并不可靠。
📌 阅读说明:根据课程 PDF《第10讲:操作符详解》第 1~5 节整理。示意图用常见的 8 位或 32 位模型帮助理解;除非另行说明,示例按 C11 编写。下篇继续讲逗号表达式、[]、()、结构成员访问、类型转换与表达式求值。
文章目录
- 一、先把操作符放到一张地图里
- 二、不同进制只是同一个数的不同写法
- 三、原码、反码、补码:分清教学模型与语言规则
- 四、移位操作符:先看类型,再看移动位数
- 五、四个位操作符怎样逐位计算
- 六、实战一:统计二进制中 1 的个数
- 七、实战二:指定二进制位的置 1、清 0 与翻转
- 八、常见误区与练习
- 总结
一、先把操作符放到一张地图里
C 语言操作符很多,先按用途建立印象,遇到表达式时再问“它处理的是数值、逻辑,还是二进制位”。
| 算术 | + – * / % | 四则运算、求余 |
| 移位 | << >> | 移动整数的二进制位 |
| 按位 | `& | ^ ~` |
| 赋值 | `= += -= *= /= %= <<= >>= &= | = ^=` |
| 比较与逻辑 | `< <= > >= == != && | |
| 其他 | ?: , [] () . -> sizeof | 条件选择、顺序组合、访问与调用等 |
同一个符号可能承担不同角色。例如 & 在 a & b 中是按位与,在 &a 中是取地址;* 可以表示乘法,也可以在指针表达式中表示间接访问。判断时应看操作数和语法位置。
这一篇集中处理与二进制相关的操作符。先学进制,是因为位运算不是“魔法符号”:它处理的就是整数值中的各个位。

二、不同进制只是同一个数的不同写法
十进制 15、二进制 1111、八进制 17、十六进制 F 说的是同一个数值。区别是每位可用的数字和每位权重。
2.1 用“位权”读二进制
十进制的 123 可以展开为 1×10² + 2×10¹ + 3×10⁰。二进制把底数换成 2:
(1101)₂ = 1×2³ + 1×2² + 0×2¹ + 1×2⁰
= 8 + 4 + 0 + 1
= (13)₁₀
从右向左依次是 1、2、4、8、16……,每往左一位,权重翻倍。计算时先写权重,再把为 1 的位置加起来,就不容易漏位。

2.2 十进制怎样转成二进制
以十进制 13 为例,反复除以 2,记录余数,最后倒序读取:
13 ÷ 2 = 6 余 1
6 ÷ 2 = 3 余 0
3 ÷ 2 = 1 余 1
1 ÷ 2 = 0 余 1
从下往上:1101
这个方法适合非负整数。写程序时还要区分“数值转换”和“把一个整数以某种格式输出”;底层数值没有“自己带着十进制标签”。
2.3 二进制与八、十六进制:每 3 位或 4 位一组
由于 8 = 2³、16 = 2⁴,从右向左分组即可:
二进制:0110 1011
十六进制: 6 B → 0x6B
二进制:01 101 011
八进制: 1 5 3 → 0153
两种写法都表示十进制 107。左侧不足一组时可以补前导 0 帮助对齐,它不改变数值。C 语言里的整数常量 0153 以 0 开头,表示八进制;0x6B 以 0x 开头,表示十六进制。特别留意 08 不是合法的八进制整数常量。
#include <stdio.h>
int main(void)
{
unsigned int value = 107U;
printf("十进制=%u,八进制=%o,十六进制=%X\\n",
value, value, value);
return 0;
}
输出是 十进制=107,八进制=153,十六进制=6B。%o 和 %X 只改变显示方式,不改变 value 保存的数值。
三、原码、反码、补码:分清教学模型与语言规则
课程用原码、反码、补码解释有符号整数。这个模型很适合建立直觉,但阅读代码时要记住:本篇按照 C11 讲解,不能把“所有平台的有符号整数都必然是 32 位补码”当成语言保证。 数据位数、char 是否带符号,以及 C11 允许的有符号整数表示方式,都可能影响具体位图。
下面固定一个8 位补码教学模型,只讨论 +5 和 -5:
+5 原码:0000 0101
+5 反码:0000 0101
+5 补码:0000 0101
-5 原码:1000 0101 最高位标记负数
-5 反码:1111 1010 符号位不变,其他位取反
-5 补码:1111 1011 反码再加 1
补码的一个好处是:在固定宽度下,加法与减法可以统一到二进制加法,并自然舍弃最高位以外的进位。比如按 8 位演示,5 + (-5) 的低 8 位为 0000 0000。这解释了补码为何在实际机器中常见。

容易混淆的三件事:
四、移位操作符:先看类型,再看移动位数
<< 和 >> 的操作数必须是整数类型。移位前整数提升会发生,结果类型是提升后的左操作数类型。右边的移位位数必须 >= 0,且严格小于提升后左操作数类型的位宽;否则行为未定义。它不是“自动对位宽取模”。
4.1 左移:右侧补 0,但结果仍受类型规则约束
十进制 10:0000 1010
左移一位:0001 0100 → 20
unsigned int number = 10U;
unsigned int doubled = number << 1;
无符号左移相当于乘以 2 的相应幂,并按无符号类型的模数保留结果。对于非负的有符号数,只有结果能用该有符号类型表示时,才适合按“乘以 2”理解;负数左移、无法表示的结果都不应这样写。
number << 1 不会修改 number 本身,赋值给 doubled 才保存新结果;若要改原变量,可写 number <<= 1。
4.2 右移:无符号数左侧补 0
十进制 10:0000 1010
右移一位:0000 0101 → 5
unsigned int number = 10U;
unsigned int half = number >> 1;
无符号右移可理解为除以 2 的相应幂并向下取整。负的有符号数右移在常见机器上可能做符号扩展,但 C11 对它的结果规定为实现定义,不要把某台机器上的输出写成跨平台结论。

4.3 写移位代码时先过三关
| 左操作数是什么类型? | 掩码和位字段优先用 unsigned int 或明确宽度的无符号类型 |
| 位数是否合法? | 0 <= shift < 位宽,动态位数要先检查 |
| 结果会不会丢高位? | 无符号丢位有明确规则,但业务是否允许仍要检查 |
课程中的 num >> -1 正是非法位数的例子。另一个常见错误是把 1 << 31 当成通用的 32 位最高位掩码:1 是有符号 int,这样的写法会碰到有符号移位规则;至少应先从 1U 或合适的无符号类型出发,并检查位宽。
五、四个位操作符怎样逐位计算
按位运算处理的是整数的对应位;它们与 &&、||、! 等逻辑运算不是一回事。先用单个位建立真值表:
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 1 | 1 |
| 1 | 0 | 0 | 1 | 1 |
| 1 | 1 | 1 | 1 | 0 |
- &:都为 1 才为 1,适合筛选指定的位。
- |:有一个为 1 就为 1,适合把指定位设为 1。
- ^:两位不同 为 1,适合翻转指定位。
- ~:一元运算符,逐位取反。
1010 (10)
& 1100 (12)
—-
1000 (8)
1010
| 1100
—-
1110 (14)
1010
^ 1100
—-
0110 (6)

使用 ~ 时要把类型宽度与整数提升放进脑中。比如 ~0U 的结果是该 unsigned int 类型所有值位都为 1,其值为 UINT_MAX;~0 则是有符号 int 表达式,不能把两者的类型和输出格式混为一谈。
5.1 异或交换:能做,但没必要用它写业务代码
课程给出“不能创建第三个变量”的面试题:
a ^= b;
b ^= a;
a ^= b;
当 a、b 是两个不同的整数对象时,这个技巧可以交换数值。若通过两个实际指向同一对象的别名去执行,会把值清零;它还不如临时变量清楚。实际代码直接写:
int temporary = a;
a = b;
b = temporary;
可读性通常比省掉一个临时对象更重要。
六、实战一:统计二进制中 1 的个数
设目标是统计一个 unsigned int 值在自身位宽内有多少个 1。课程依次展示了除 2、逐位测试、n & (n-1) 三种思路;把输入明确成无符号后,就能避免负数除法和有符号移位带来的歧义。
6.1 方法一:不断取最低位
unsigned int count_ones_by_shift(unsigned int value)
{
unsigned int count = 0U;
while (value != 0U)
{
count += value & 1U;
value >>= 1;
}
return count;
}
每轮用 value & 1U 读取最低位,再右移。时间与需要检查的有效位数相关,最多检查该类型的位宽。
6.2 方法二:逐位检测,留意移位的类型
也可以按位号循环,用 value & (1U << i) 检测。但要保证 i 小于 unsigned int 的位宽。写成固定 32 次隐含了“unsigned int 正好 32 位”的平台假设;写成 1 << i 又会引入有符号左移的问题。
6.3 方法三:每轮清除最右边的一个 1
unsigned int count_ones(unsigned int value)
{
unsigned int count = 0U;
while (value != 0U)
{
value &= value – 1U;
++count;
}
return count;
}
为什么有效?假设 value = 0b10110000。减 1 后,最右边的 1 变成 0,它右边的 0 变成 1;再按位与,右侧这一段都被清掉,原来最右边的 1 也被清掉:
value 1011 0000
value – 1 1010 1111
与运算 1010 0000
因此循环次数等于 1 的个数,而非总位宽。输入为 0 时直接返回 0。这里有意使用 unsigned int:若把课程中的 int num = -1; num = num & (num – 1); 机械套到所有负数,num – 1 等运算可能触及有符号溢出,不适合作为通用解法。

完整可运行示例:
#include <stdio.h>
unsigned int count_ones(unsigned int value)
{
unsigned int count = 0U;
while (value != 0U)
{
value &= value – 1U;
++count;
}
return count;
}
int main(void)
{
printf("%u\\n", count_ones(0U)); /* 0 */
printf("%u\\n", count_ones(13U)); /* 3:1101 */
printf("%u\\n", count_ones(15U)); /* 4:1111 */
return 0;
}
七、实战二:指定二进制位的置 1、清 0 与翻转
课程要求把十进制 13 的从右往左第 5 位改成 1,再改回 0。若位号从 0 开始,这就是第 4 位,掩码写为 1U << 4:
原值: 0000 1101 (13)
掩码: 0001 0000 (1U << 4)
按位或: 0001 1101 (29)
三个常用操作可以合成一张“掩码速查表”:
| 置 1 | value |= mask | 13 → 29 |
| 清 0 | value &= ~mask | 29 → 13 |
| 翻转 | value ^= mask | 13 ↔ 29 |
| 读取 | (value & mask) != 0U | 13 的第 4 位为 0 |

#include <stdio.h>
int main(void)
{
unsigned int value = 13U;
unsigned int mask = 1U << 4;
value |= mask;
printf("置 1:%u\\n", value); /* 29 */
value &= ~mask;
printf("清 0:%u\\n", value); /* 13 */
value ^= mask;
printf("翻转:%u\\n", value); /* 29 */
return 0;
}
如果位号来自用户输入,先检查它小于 unsigned int 的位宽。检查可以用 sizeof(unsigned int) * CHAR_BIT 计算对象的位数;更严格地讨论无符号值位宽时,还需考虑类型表示和填充位。对于本文的 1U << 4,位号固定且显然合法。
八、常见误区与练习
| & 和 && 都是“与” | 前者逐位运算;后者把表达式当真假值处理,并可能短路 |
| x << 1 一定等于 x * 2 | 要考虑类型、负数、可表示范围与高位丢失 |
| x >> -1 会“往反方向移动” | 负的移位位数导致未定义行为 |
| 负 int 右移总是补 1 | C11 把负数右移的结果交给实现定义 |
| 统计 1 时写 1 << 31 | 1 是有符号 int;要从无符号类型出发并核对位宽 |
| 位运算复杂就更高效 | 先比较算法和可读性,编译器也会优化清楚的写法 |
动手练习:
参考答案:42 = (101010)₂ = (52)₈ = (2A)₁₆;三个位运算结果依次为 8、14、6;40 → 32 → 0,所以循环执行 2 次。
总结
读位运算时,建议按固定顺序思考:先确认整数类型和位宽,再写出需要的二进制位,最后检查移位位数与结果范围。这样,&、|、^、~ 就是清楚的逐位规则,掩码也能自然推导出来。
下篇会继续看那些不直接处理二进制位、却常常让表达式变难读的操作符:逗号、下标、函数调用、结构成员访问,以及优先级与实际求值顺序的区别。
资料:课程 PDF《第10讲:操作符详解》;ISO C11 工作草案 N1570 的整数转换、移位与按位运算条款。本文示例和示意图为重新整理与绘制。
网硕互联帮助中心




评论前必须登录!
注册