云计算百科
云计算领域专业知识百科平台

【C 语言】操作符详解(上):进制、补码、移位与位运算实战

🔥 星光编译者 · 个人主页

📚 学习专栏: 《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。这解释了补码为何在实际机器中常见。

在这里插入图片描述

容易混淆的三件事:

  • “显示成负数”是数值解释;“位图是什么”涉及类型宽度和表示方式。
  • 把负的 int 转成 unsigned int,C 语言对结果有明确的模 UINT_MAX + 1 转换规则;不要仅靠想象内存中某些字节做推导。
  • 对负的有符号数做右移,结果由实现定义;需要稳定的逻辑位运算时,优先使用无符号类型。

  • 四、移位操作符:先看类型,再看移动位数

    << 和 >> 的操作数必须是整数类型。移位前整数提升会发生,结果类型是提升后的左操作数类型。右边的移位位数必须 >= 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 或合适的无符号类型出发,并检查位宽。


    五、四个位操作符怎样逐位计算

    按位运算处理的是整数的对应位;它们与 &&、||、! 等逻辑运算不是一回事。先用单个位建立真值表:

    aba & ba | ba ^ b
    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)

    三个常用操作可以合成一张“掩码速查表”:

    目标表达式以第 4 位为例
    置 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 分别写成二、八、十六进制,再用 printf 核对。
  • 不运行代码,计算 10U & 12U、10U | 12U、10U ^ 12U。
  • 用 unsigned int 写一个函数,检测给定合法位号上的位是否为 1。
  • 跟踪 count_ones(40U):40 的二进制为 101000,循环执行几次?每次 value 变成多少?
  • 参考答案:42 = (101010)₂ = (52)₈ = (2A)₁₆;三个位运算结果依次为 8、14、6;40 → 32 → 0,所以循环执行 2 次。


    总结

    读位运算时,建议按固定顺序思考:先确认整数类型和位宽,再写出需要的二进制位,最后检查移位位数与结果范围。这样,&、|、^、~ 就是清楚的逐位规则,掩码也能自然推导出来。

    下篇会继续看那些不直接处理二进制位、却常常让表达式变难读的操作符:逗号、下标、函数调用、结构成员访问,以及优先级与实际求值顺序的区别。

    资料:课程 PDF《第10讲:操作符详解》;ISO C11 工作草案 N1570 的整数转换、移位与按位运算条款。本文示例和示意图为重新整理与绘制。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【C 语言】操作符详解(上):进制、补码、移位与位运算实战
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!