一、位运算基础:原码、反码、补码与六种位运算符详解

各位同学,咱们今天聊点硬核的。位运算,说白了就是直接操作二进制位。很多初学者觉得这东西离自己很远,其实不然。我做了十几年嵌入式开发,可以负责任地告诉你:不懂位运算,你写不出高效的底层代码。

先问大家一个问题:为什么计算机要用补码来存储整数?嗯,这个问题我当年也困惑了很久。今天咱们就把原码、反码、补码这"三兄弟"彻底讲明白。

1.1 原码、反码、补码——计算机的"三原色"

先看一个最简单的例子:+5-5 在计算机里怎么表示?

原码:最高位是符号位(0正1负),其余位是数值的绝对值。

  • +5 的原码(8位):0000 0101
  • -5 的原码(8位):1000 0101

反码:正数的反码等于原码;负数的反码是符号位不变,其余位取反。

  • +5 的反码:0000 0101
  • -5 的反码:1111 1010

补码:正数的补码等于原码;负数的补码是反码加1。

  • +5 的补码:0000 0101
  • -5 的补码:1111 1011

核心结论:计算机中存储的是补码。为什么?因为补码可以把减法统一成加法。你想想看,CPU里只需要加法器就够了,省了多少晶体管!

我记得刚入行时,有个同事死活想不通:-5 的补码为什么是 1111 1011?我让他算一下 5 + (-5)

  0000 0101  (+5的补码)
+ 1111 1011  (-5的补码)
= 1 0000 0000  (进位被丢弃,结果为0)

看到没?结果正好是0。这就是补码的精妙之处。

1.2 原码、反码、补码的转换规则

转换其实很简单,记住这张表就够了:

数值 原码 反码 补码
+7 0000 0111 0000 0111 0000 0111
-7 1000 0111 1111 1000 1111 1001
+0 0000 0000 0000 0000 0000 0000
-0 1000 0000 1111 1111 0000 0000

避坑指南:我曾经在项目中遇到过一个bug,就是因为把负数的原码直接当补码用了。结果一个简单的减法运算,算出来的值完全不对。排查了整整一个下午才发现问题。所以记住:运算时一定要用补码

1.3 C语言中的六种位运算符

好,现在咱们进入正题。C语言提供了6种位运算符,我一个个讲。

1.3.1 按位与(&)

规则:两个位都是1,结果才是1。说白了就是"有0则0"。

unsigned char a = 0x0F;  // 0000 1111
unsigned char b = 0x33;  // 0011 0011
unsigned char c = a & b; // 0000 0011 = 0x03

我在项目中常用它来清零某些位。比如你想把某个寄存器的低4位清零,高位不变:reg &= 0xF0;

1.3.2 按位或(|)

规则:两个位只要有一个是1,结果就是1。也就是"有1则1"。

unsigned char a = 0x0F;  // 0000 1111
unsigned char b = 0x30;  // 0011 0000
unsigned char c = a | b; // 0011 1111 = 0x3F

这个我经常用来置位。比如点亮LED:PORT |= (1 << 3); 把第3位置1。

1.3.3 按位异或(^)

规则:两个位相同为0,不同为1。这个有点意思。

unsigned char a = 0x0F;  // 0000 1111
unsigned char b = 0x33;  // 0011 0011
unsigned char c = a ^ b; // 0011 1100 = 0x3C

异或有个经典用法:交换两个变量的值,不用临时变量

a = a ^ b;
b = a ^ b;
a = a ^ b;

嗯,这个技巧面试经常考。不过我个人建议,日常开发还是用临时变量吧,可读性更重要。

1.3.4 按位取反(~)

规则:0变1,1变0。单目运算符,只有一个操作数。

unsigned char a = 0x0F;  // 0000 1111
unsigned char b = ~a;    // 1111 0000 = 0xF0

注意:取反操作会提升到int类型。比如 ~0x0F 在32位系统上是 0xFFFFFFF0,不是 0xF0。我曾经因为这个坑,在给8位单片机写寄存器时出了大问题。

1.3.5 左移(<<)

规则:所有位向左移动,右边补0。左移n位相当于乘以2的n次方。

unsigned char a = 0x05;  // 0000 0101 = 5
unsigned char b = a << 2; // 0001 0100 = 20

左移在嵌入式里太常用了。比如设置GPIO引脚:GPIO->BSRR = (1 << 8); 把第8位置1。

1.3.6 右移(>>)

规则:所有位向右移动。对于无符号数,左边补0;对于有符号数,行为由编译器决定(通常是算术右移,补符号位)。

unsigned char a = 0x20;  // 0010 0000 = 32
unsigned char b = a >> 2; // 0000 1000 = 8

避坑指南:有符号数的右移,不同编译器行为可能不同。我建议:对有符号数做位运算时,先转成无符号类型。这是血的教训换来的经验。

1.4 知识体系总览

下面这张图,把本章的核心知识点串起来了。你仔细看看,就能明白它们之间的关系。

位运算知识体系 计算机中整数的三种编码 原码 反码 补码(存储用) C语言六种位运算符 & 按位与 | 按位或 ^ 按位异或 ~ 按位取反 << 左移 >> 右移 典型应用:寄存器操作、位掩码、数据压缩、状态标志

1.5 实战小技巧

最后,分享几个我常用的位操作技巧:

  1. 判断奇偶if (x & 1) 为真则是奇数。比取模快多了。
  2. 交换高低字节val = (val >> 8) | (val << 8); 16位数据高低字节互换。
  3. 计算2的幂1 << n 就是2的n次方。
  4. 取模2的幂x & (n-1) 等价于 x % n,前提是n是2的幂。

总结一下:原码、反码、补码是基础,六种位运算符是工具。基础打牢了,工具用熟了,你就能写出高效、优雅的底层代码。我见过太多人,写了几年C语言,位运算还停留在"听说过"的阶段。希望你不是其中之一。


公众号:蓝海资料掘金营,微信deep3321