4、移位运算技巧:左移(<<)与右移(>>)的算术与逻辑区别,快速乘除2的幂
移位运算,说白了就是让二进制位「搬家」。左移往高位搬,右移往低位搬。听起来简单,但这里头有个坑——右移分两种:算术右移和逻辑右移。我当年刚入行时,就在这上面栽过跟头。
4.1 左移运算:<<
左移的规则很直白:所有位向左移动,右边补0。每左移一位,数值就乘以2。
// 左移示例
unsigned char a = 0x05; // 二进制: 0000 0101
unsigned char b = a << 1; // 结果: 0000 1010 = 0x0A = 10
unsigned char c = a << 2; // 结果: 0001 0100 = 0x14 = 20
你看,5左移1位变成10,左移2位变成20。这就是快速乘2的幂。
核心公式:x << n 等价于 x × 2ⁿ
但要注意,左移可能溢出。比如一个8位无符号数,最高位被移出去就丢了。我在项目中遇到过这种情况:用左移做乘法时,没注意数据范围,结果算出来的值莫名其妙变小了。排查了半天才发现是溢出。
警告:左移不能超过数据类型的位宽。比如32位int,左移32位是未定义行为。编译器可能给你一个意想不到的结果。
4.2 右移运算:>>
右移就复杂了。它分两种情况:
| 右移类型 | 高位补什么 | 适用场景 |
|---|---|---|
| 逻辑右移 | 补0 | 无符号数 |
| 算术右移 | 补符号位(原最高位) | 有符号数 |
为什么会这样?你想想看,有符号数的最高位是符号位。如果右移时补0,负数就变成正数了,这显然不对。所以算术右移保留了符号位。
// 逻辑右移 vs 算术右移
unsigned char u = 0x80; // 二进制: 1000 0000
signed char s = 0x80; // 二进制: 1000 0000 (补码表示-128)
unsigned char u_result = u >> 1; // 逻辑右移: 0100 0000 = 64
signed char s_result = s >> 1; // 算术右移: 1100 0000 = -64
看到了吗?同样的二进制数,右移结果完全不同。无符号数右移1位变成64,有符号数右移1位变成-64。
个人习惯:我写代码时,只要涉及右移,一律用无符号数。除非你明确知道自己在做有符号数的算术右移。这样可以避免很多隐晦的bug。
4.3 快速乘除2的幂
移位比乘除法快得多。在嵌入式系统里,这招特别实用。
// 快速乘除2的幂
int x = 100;
int y1 = x << 3; // 乘以8: 100 * 8 = 800
int y2 = x >> 2; // 除以4: 100 / 4 = 25
// 注意:右移做除法只对正数有效
int z = -100;
int z1 = z >> 2; // -100 / 4 = -25? 不一定!
嗯,这里要注意。负数右移做除法,结果可能不是你想要的。因为算术右移是向下取整,而C语言的整数除法是向零取整。两者有区别。
// 负数右移 vs 除法
int a = -7;
int b = a >> 2; // 算术右移: -2 (向下取整)
int c = a / 4; // 整数除法: -1 (向零取整)
我曾经在写一个音频算法时,用右移代替除法来优化性能。结果负数处理出了问题,声音出现了毛刺。排查了好久才发现是取整方向不一致。
避坑指南:用右移做除法时,确保操作数是非负数。或者用无符号类型。否则结果可能和预期不一样。
4.4 实际应用场景
移位运算在嵌入式开发中无处不在。我列几个常见的:
- 寄存器位操作:设置某一位用左移,清除某一位用左移取反
- 数据打包解包:把多个小数据拼成一个大数据
- 颜色值处理:RGB颜色分离和合并
- 性能优化:用移位代替乘除法
// 寄存器位操作示例
#define GPIO_PORTB *(volatile unsigned int*)0x4000C000
#define LED_PIN 5
// 设置第5位为1
GPIO_PORTB |= (1 << LED_PIN);
// 清除第5位
GPIO_PORTB &= ~(1 << LED_PIN);
// 读取第5位
int led_state = (GPIO_PORTB >> LED_PIN) & 1;
你看,这些操作都离不开移位。左移用来生成位掩码,右移用来提取位值。
4.5 知识体系总览
下面这张图总结了移位运算的核心知识点:
4.6 总结
移位运算看着简单,但用好了能写出高效又优雅的代码。我个人的经验是:
- 左移做乘法,右移做除法——但只对正数有效
- 无符号数右移是逻辑右移,有符号数右移是算术右移
- 移位比乘除法快一个数量级,在循环里用效果明显
- 注意移位位数不要超过类型位宽,否则是未定义行为
说白了,移位就是二进制世界的「搬家」。搬对了,效率翻倍;搬错了,bug难找。多写写代码,多踩踩坑,自然就熟练了。
公众号:蓝海资料掘金营,微信deep3321