当前位置: 首页 > news >正文

位运算基础应用

一、位运算概述

在计算机系统中,数据最终都以二进制形式存储。无论是一个普通整型变量,还是一个寄存器值,处理器看到的本质都是由若干个 0 和 1 组成的二进制位序列。位运算就是直接针对这些二进制位进行操作的一类运算。

位运算和加减乘除这类算术运算不同,位运算并不优先关注“数值大小”的数字意义,而是直接关注一个数据在二进制层面的每一位状态。例如,某一位是 1 还是 0,某几位组成的字段代表什么含义,某一位是否需要被置位或清零,这些都属于位运算处理的范畴。

在C语言中,位运算主要包括以下几类:

运算符名称说明
&按位与两位都为 1,结果才为 1
|按位或两位任一位为1,结果为1
^按位异或两位不同结果为 1,相同为 0
~按位取反将每一位 0/1 反转
<<左移所有位整体左移若干位
>>右移所有位整体右移若干位

位运算在嵌入式开发中是非常基础的一项能力,因为底层硬件配置本身就是按位定义的。比如:

这个是STM32F4xx 的GPIO位定义,从中我们可以看出 31:16 是预留位,而 15:0 则是每个GPIO引脚的输出类型,每个位分别对应引脚 Px0、Px1、Px2 ··· Px14、Px15,这种情况下,我们就可以通过位运算对 Px0 ~ Px15 中的特定位进行读取、修改和组合。

从这里我们也可以看出位运算比较核心的价值:

  • 可以精确控制单个 bit 或一组 bit;
  • 节省存储空间,可以用一个字节表达多个状态;
  • 非常适合寄存器、协议、标志位这类底层数据结构的处理。

所以位运算不是“语法技巧”,而是底层开发中的常规工具。

二、位运算基础原理

(一)如何按位看数据

给一个8位十进制数据 uint8_t a = 13,转换成二进制为 0000 1101(13=1x20+0x21+1x22+1x23),如果按 bit 编号,一般从低位到高位编号为 bit0 ~ bit7:

bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0 0 0 0 0 1 1 0 1

在这里可以看到 bit0=1、bit1=0、bit2=1、bit3=1。通过这也可以看出位运算本质上就是对这样的位序列逐位进行逻辑处理。

(二)按位与 &

运算规则:两个操作数对应位都为1,结果才为1

0 & 0 = 0 0 & 1 = 0 1 & 0 = 0 1 & 1 = 1

下面举个具体示例来进行说明:

uint8_t a = 0b1101; uint8_t b = 0b1011; uint8_t c = a & b;

对 a 与 b 逐位对比:

a: 1 1 0 1 b: 1 0 1 1 a&b: 1 0 0 1

所以最终 c 的结果就是 0b1001,这就是按位与的执行逻辑。在实际应用中,按位与最典型的用途不是“求值”,而是屏蔽不关心的位,也就是后面常说的 mask 操作。

(三)按位或 |

运算规则:对应位只要有一个是1,结果就是1

0 | 0 = 0 0 | 1 = 1 1 | 0 = 1 1 | 1 = 1

继续按照按位与中的示例来进行说明:

a: 1 1 0 1 b: 1 0 1 1 a|b: 1 1 1 1

通过按位或运算后,最终 c 的结果变为了 0b1111,在实际使用中,我们也经常通过按位或去达到对某一bit进行置位的目的

(四)按位异或 ^

运算规则:对应位不同为1,相同为0

0 ^ 0 = 0 0 ^ 1 = 1 1 ^ 0 = 1 1 ^ 1 = 0

继续按照按位与中的示例进行说明:

a: 1 1 0 1 b: 1 0 1 1 a^b: 0 1 1 0

通过按位异或运算后,最终 c 的结果变成了 0b0110。所以对按位异或可以总结出以下通式:

//x表示任意整数变量 x ^ 0 = x x ^ x = 0

(五)按位取反 ~

运算规则:每一位都翻转

1 = 0000 0001 ~1 = 1111 1110 0 = 0000 0000 ~0 = 1111 1111

从这个示例可以看出,对 1 或 0 进行 ~ 时是针对其数据类型的整个类型宽度进行取反的,而不是只关心显式的那几位bit,比如:

//uint8_t 1 = 0000 0001 ~1 = 1111 1110 0 = 0000 0000 ~0 = 1111 1111 //uint16_t 1 = 0000 0000 0000 0001 ~1 = 1111 1111 1111 1110 0 = 0000 0000 0000 0000 ~0 = 1111 1111 1111 1111

(六)左移 <<

运算规则:将所有 bit 整体向左移动 n 位,高位溢出的bit丢弃,低位补0

1010 1010 << 1 = 0101 0100

对于无符号数,在不溢出的前提下:

x << n ≈ x * 2^n

比如:

5 << 1 = 10 5 << 2 = 20 5 << 3 = 40

这个并不是所有场景都可以将左移等同乘法,只要发生溢出,高位被丢弃,结果就不再等价。比如:

//二进制1010 1010 = 十进制170 1010 1010 << 1 /* 按照左移乘法的公式来算,8位整数只能表示0~255, 所以但从范围边界来看,移位后的结果也一定发生 了截断,即大于0小于255 */ 170 * 2 = 340 /* 移位后可以得到0101 0100,再转 十进制84,而84与340不等,所以若 发生溢出就无法再使用<<去做乘法。 */ 1010 1010 << 1 = 0101 0100

而在C标准中,如果有符号数左移后溢出导致结果不能表示,就会产生未定义行为。所以在实际工程中,一般更好是对 unsigned 类型的数据进行左移操作

(七)右移 >>

运算规则:将所有 bit 整体向右移动 n 位,低位移除的 bit 被丢弃

0000 1101 >> 1 = 0000 0110

可以看到,0000 1101 最右边的 1 被移出丢弃后变成了 0000 0110。

对于无符号数:

//x是任意无符号整数 x >> n ≈ x / 2^n

需要注意右移与左移不同,右移有两种情况,一种是逻辑右移,一种是算术右移,而左移则只有一种情况,并没有做区分。那么为什么右移会出现这两种情况呢,主要还是因为右移涉及到了有符号整形中的符号变化问题。

无符号数(unsigned)中,右移整形只需要在高位补0即可,比如

unsigned char a = 0b10101010; a >> 1 = 0b0101 0101;

这是逻辑右移

有符号(signed)中,右移整形大多数编译器会在高位补符号位,比如:

1110 0000 >> 1 //若是负数,高位补1 = 1111 0000 //若是正数,高位补0 = 0111 0000

这是算术右移

针对左移和右移可做如下总结:

运算补位特殊情况
<<补0可能溢出
>> unsigned补0逻辑右移
>> signed补符号位算术右移

补充一句:在补码系统中,左移相当于乘2,右移相当于除2,但只有在不发生溢出且使用算术右移时才能保持符号正确。所以在实际开发中,更推荐使用无符号数(unsigned)进行位运算。

(八)运算符优先级

位运算符在整体运算符体系中的优先级如下:

优先级顺序运算符名称
~按位取反
<<>>移位
&按位与
^按位异或
|按位或

位运算符与其他运算符的优先级关系如下:

优先级运算符名称
~!单目运算符
*/%乘除
+-加减
<<>>移位
<<=>>=比较
==!=相等
&按位与
^按位异或
|按位或
&&逻辑与
||逻辑或

所以总体看下来就是位运算优先级低于算术运算,但高于逻辑运算

三、位运算常用操作

这一部分时位运算在工程中最常用的四类基本动作:置位、清零、读取、翻转

(一)置位

在对寄存器进行操作时,我们通常需要将寄存器的第 n 个 bit 置1,此时我们可以

reg |= (1U << n);

比如现在我想把 reg 的 bit3 置1,其他位保持不变,就可以

reg |= (1U << 3);

下面我会对这个 reg 的 bit3 置位操作进行拆解。

首先 1U << 3 可以得到:

1U = 0000 0001 -> 1U << 3 -> 0000 0001 << 3 -> 0000 1000

然后再和原值做按位或对应位只要有一个是1,结果就是1):

原值: 1010 0010 掩码: 0000 1000 结果: 1010 1010

从运算结果来看,实现了将 bit3 强制置1的效果。

(二)清零

清零,即将第 n 个 bit 清0

reg &= ~(1U << n);

比如我现在想把 reg 的 bit3 清零,其他位保持不变,就可以

reg &= ~(1U << 3);

下面我会依据这个 reg 的 bit3 清零操作进行拆解:

首先 1U << 3 可以得到:

1U = 0000 0001 -> 1U << 3 -> 0000 0001 << 3 -> 0000 1000

然后对(1U << 3)进行取反将每一位0/1反转):

~(1U << 3) -> ~0000 1000 -> 1111 0111

最后再和原值做按位与两位都为 1,结果才为 1):

原值:1010 1111 掩码:1111 0111 结果:1010 0111

只有 bit3 被清0。

(三)读取

读取,即判断第 n 个 bit 是否为1

if(reg & (1U << n)) { //bit n 为1 }

比如:

if(reg & (1U << 5)) { //第五位标志有效 }

意思是如果 bit5 原本是1,那么与之后结果非0;但是若 bit5 原本为0,那么结果为0。

(四)翻转

翻转,即将第 n 个 bit 翻转

reg ^= (1U << n);

比如:

reg ^= (1U << 3);

下面我将把 reg 的 bit3 翻转操作进行拆解。

首先是 1U << 3 可以得到:

1U << 3 -> 0000 0001 << 3 -> 0000 1000

然后和原值做按位异或两位不同结果为 1,相同为 0):

情况1(bit3 为1): 原值:1010 1010 掩码:0000 1000 结果:1010 0010 情况2(bit3 为0): 原值:1010 0010 掩码:0000 1000 结果:1010 1010

可以看到情况1中原值的 bit3 原来是1,然后被翻转为了0;情况2原值的 bit3 为0,然后被翻转为了1。

(五)多位同时操作

不是只有单 bit 才能操作,一组 bit 同样可以使用位运算进行处理。比如:

(1)设置低四位为1:

reg |= 0x0F;

(2)清低四位:

reg &= ~0x0F;

(3)读取低四位:

value = reg & 0x0F;
http://www.cnnetsun.cn/news/1710892.html

相关文章:

  • 第28课:Qt 读系统时钟并响应中断,让时间界面和板级事件同时在线
  • 告别B站资源无法保存的烦恼:BiliTools跨平台工具箱完整使用指南
  • 如何用OpCore-Simplify在30分钟内完成黑苹果配置:自动化OpenCore EFI工具终极指南
  • FUXA SVG编辑器元素管理功能优化:从问题发现到价值验证
  • 第6章 数据类型转换-6.8 转换为集合
  • 样本收集的致命误区:为什么你的AI模型“一上产线就拉胯”?
  • 深入理解 Firebase onSnapshot 的监听机制
  • 模电实战-比较器正反馈接法的窗口电压设计
  • 告别繁琐下载:File Browser极简方案实现20+格式文件在线预览
  • 基于Logisim与Verilog HDL的运动码表计时电路设计与DE2-70开发板验证
  • 别再用手机思维做TV App了!Android TV开发必知的模拟器操作与UI焦点设计实战
  • 别只盯着stkInit!用这个STK MATLAB互联测试脚本,一键验证你的环境是否真的配好了
  • 魔兽争霸3 Windows 11兼容性终极解决方案:让你的经典游戏重获新生
  • 终极Limbus Company自动化助手:5大功能彻底解放你的双手
  • 终极指南:如何快速上手ALOHA开源双臂机器人系统,开启你的机器人开发之旅
  • 基于元模型优化的虚拟电厂主从博弈动态定价与能量管理双层调度策略
  • ai辅助开发新体验:让快马ai帮你打造智能win10安装准备助手
  • AI辅助开发性能代码:让快马平台AI成为你的高性能并发任务调度顾问
  • Windows 批量文件夹图标设置工具(支持.ico.exe 图标提取与替换)自动扫描每个文件夹中的ICO和EXE图标文件
  • 智能自动化任务管理器是专业 Windows 自动化工具,零代码可视化配置,支持全类型任务与多模式执行,内置键鼠编辑器
  • 全面掌握HSTracker:从炉石传说套牌追踪到高级数据分析的实战指南
  • 深入剖析Golang HTTP/2客户端连接池与多路复用机制
  • TCP Keep-Alive、HTTP Keep-Alive、应用层心跳,傻傻分不清?一张图讲透网络‘保活’全家桶
  • U盘启动盘制作全攻略:从Rufus到Ventoy的深度对比
  • 3步精通UndertaleModTool:解锁GameMaker游戏修改全流程
  • 抖音批量下载全攻略:一键保存无水印视频,轻松管理海量内容
  • 高光谱成像的噪声估计
  • 2025届毕业生推荐的五大AI辅助论文平台推荐榜单
  • 虚拟机中的AI实验室:VMware安装Ubuntu并部署Phi-4-mini-reasoning
  • IC面试官最爱问的Verilog分频题:手把手教你写出50%占空比的奇偶分频代码