一、位运算基础:原码、反码、补码与六种位运算符详解
各位同学,咱们今天聊点硬核的。位运算,说白了就是直接操作二进制位。很多初学者觉得这东西离自己很远,其实不然。我做了十几年嵌入式开发,可以负责任地告诉你:不懂位运算,你写不出高效的底层代码。
先问大家一个问题:为什么计算机要用补码来存储整数?嗯,这个问题我当年也困惑了很久。今天咱们就把原码、反码、补码这"三兄弟"彻底讲明白。
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 知识体系总览
下面这张图,把本章的核心知识点串起来了。你仔细看看,就能明白它们之间的关系。
1.5 实战小技巧
最后,分享几个我常用的位操作技巧:
- 判断奇偶:
if (x & 1)为真则是奇数。比取模快多了。 - 交换高低字节:
val = (val >> 8) | (val << 8);16位数据高低字节互换。 - 计算2的幂:
1 << n就是2的n次方。 - 取模2的幂:
x & (n-1)等价于x % n,前提是n是2的幂。
总结一下:原码、反码、补码是基础,六种位运算符是工具。基础打牢了,工具用熟了,你就能写出高效、优雅的底层代码。我见过太多人,写了几年C语言,位运算还停留在"听说过"的阶段。希望你不是其中之一。
公众号:蓝海资料掘金营,微信deep3321