移位操作:左移、右移、算术移位与逻辑移位、循环移位

移位操作,说白了就是让二进制位在寄存器里「搬家」。这玩意儿在嵌入式开发里太常用了——协议解析、状态机设计、位图操作,哪哪都离不开它。我刚开始学的时候觉得不就是左移右移嘛,有啥好讲的?结果踩了几个坑才发现,这里面的门道还真不少。

左移操作:最简单的乘法

左移就是把所有位往左边挪,右边补0。每左移一位,数值就翻一倍。嗯,前提是不溢出。

uint8_t a = 0x05;  // 二进制 0000 0101
uint8_t b = a << 2; // 结果 0001 0100 = 0x14 = 20

你看,5左移2位变成了20,正好是5×4。我在做PWM占空比计算时经常这么干——用左移代替乘法,比调用乘法指令快多了。不过要注意,左移可能会把高位数据挤出去,这就叫溢出。

⚠ 注意: 左移操作不会改变原变量的值。a << 2 只是返回一个新值,a本身还是5。想改变a,得写成 a = a << 2。

右移:算术移位 vs 逻辑移位

右移就复杂了。这里有个大坑——C语言标准没有规定右移是有符号还是无符号的。说白了,编译器说了算。

逻辑右移:左边补0,适用于无符号数。

uint8_t a = 0xF0;  // 1111 0000
uint8_t b = a >> 2; // 0011 1100 = 0x3C

算术右移:左边补符号位,适用于有符号数。

int8_t a = -16;    // 1111 0000
int8_t b = a >> 2; // 1111 1100 = -4(算术右移)

为什么会这样?因为算术右移要保持负数的符号不变。你想想看,-16除以4应该是-4,如果左边补0就变成正数了,那不乱套了?

我的经验: 我个人习惯在嵌入式代码里,对无符号数用逻辑右移,对有符号数用算术右移。但为了保险,我建议你写代码时明确告诉编译器你的意图——用 unsigned 类型做逻辑移位,用 signed 类型做算术移位。

循环移位:位运算里的「旋转木马」

循环移位和普通移位不一样。普通移位移出去的位就丢了,循环移位会把移出去的位补到另一边。就像旋转木马,转一圈又回来了。

C语言没有直接支持循环移位的运算符,得自己写。我在做CRC校验时经常用到这个。

// 循环左移
uint8_t rotl8(uint8_t value, uint8_t shift) {
    return (value << shift) | (value >> (8 - shift));
}

// 循环右移
uint8_t rotr8(uint8_t value, uint8_t shift) {
    return (value >> shift) | (value << (8 - shift));
}

举个例子,0x81循环左移1位:

0x81 = 1000 0001
循环左移1位 = 0000 0011 = 0x03

你看,最高位的1跑到了最低位。这在加密算法里特别有用,比如AES的列混合运算就用到了循环移位。

💡 小技巧: 我曾经在调试一个SPI协议解析时,发现数据总是对不上。查了半天,原来是接收到的数据需要循环移位才能对齐位序。从那以后,我写协议驱动时都会先确认一下数据的位序是大端还是小端。

四种移位操作对比

操作类型 补位方式 适用场景 注意事项
左移 右边补0 快速乘法、位掩码生成 注意溢出,移位位数不能超过类型宽度
逻辑右移 左边补0 无符号数除法、位提取 对有符号数使用可能产生意外结果
算术右移 左边补符号位 有符号数除法、保持符号 不同编译器行为可能不同
循环移位 移出位补到另一端 加密算法、CRC、位序调整 C语言无原生支持,需手动实现

避坑指南

我曾经在写一个I2C驱动时,用右移来提取数据中的某个位。结果在ARM和x86上跑出来的结果不一样。查了半天,原来是ARM的右移默认是算术移位,而x86是逻辑移位。从那以后,我写移位操作时都会加上类型转换,明确告诉编译器我要什么。

⚠ 重要提醒:

  • 移位位数不能大于等于类型宽度。比如uint8_t最多移7位,移8位是未定义行为。
  • 有符号数的右移是算术移位还是逻辑移位?C标准说「由实现定义」。说白了,别依赖它。
  • 循环移位在C语言里没有运算符,别想着用 <<< 或 >>>,那是某些汇编才有的。

知识体系总览

下面这张图把移位操作的整个知识结构串起来了。我画图时习惯把核心概念放在中间,然后往外发散。你看,左移和右移是基础,右移又分算术和逻辑两种,循环移位则是更高级的玩法。

移位操作 左移 右边补0 逻辑右移 左边补0 算术右移 补符号位 循环移位 移出位补另一端 四种移位操作的关系与区别

这张图把四种移位操作的关系理清楚了。左移和右移是基础,右移又根据补位方式不同分为逻辑和算术两种。循环移位则是在这个基础上加了「循环」的特性。你写代码时,先想清楚你要的是哪种,再动手。

核心要点:

  • 左移 = 乘2,注意溢出
  • 右移分两种:无符号用逻辑,有符号用算术
  • 循环移位需要手动实现,别偷懒
  • 移位位数不能超过类型宽度,这是硬性规定

好了,移位操作就讲这么多。记住一点:写代码时多想想「这个移位会带来什么后果」,尤其是跨平台的时候。我吃过这个亏,希望你不用再吃一次。

可可精品资料库