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

位运算基础应用(一)

一、位运算概述

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

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

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

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

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

image

  这个是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  bit00    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  1a&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  1a|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  1a^b:    0  1  1  0

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

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

(五)按位取反 ~

  运算规则:每一位都翻转

1 = 0000 0001
~1 = 1111 11100 = 0000 0000
~0 = 1111 1111

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

//uint8_t
1 = 0000 0001
~1 = 1111 11100 = 0000 0000
~0 = 1111 1111//uint16_t
1 = 0000 0000 0000 0001
~1 = 1111 1111 1111 11100 = 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;

【注:这里的 0x0F 本质上就是一个多 bit 掩码,即 0000 1111

http://www.jsqmd.com/news/462894/

相关文章:

  • 深入解析transformers中的DataCollatorForSeq2Seq:序列生成任务的数据处理利器
  • 优优推电话查询:业务范畴说明与信息核实指南 - 品牌推荐
  • #第八届立创电赛# 基于瑞萨R7FA2E1A72DFL的智能时钟DIY全解析:从电路设计到代码实现
  • 双系统Ubuntu 20.04下,从零部署EGO-Planner的避坑实践
  • PyTorch DDP多卡训练避坑指南:从环境配置到模型保存的完整流程
  • FLUX.1-dev Docker部署全解析:从环境准备到高级配置,一步到位
  • 做管理的三大核心:从思想到行动,打造高效能团队
  • 网页文字图片无法复制?这招连CSDN博主都在用的Markdown技巧
  • 从Flutter到KMP:为什么我们团队最终选择了Kotlin MultiPlatform?技术选型深度对比
  • ESP32-C3射频测试全栈指南:研发验证到量产落地
  • 实战指南:基于PyTorch的FSEL模型复现(含ETB模块调试技巧)
  • 聚焦喷墨印刷流量测量:2026年值得关注的超声波流量传感器品牌推荐 - 品牌2026
  • 漫画脸描述生成开发者案例:如何将Qwen3-32B接入自有AI创作平台
  • 给OpenClaw戴上“安全锁“:AI沙箱基于E2B的硬件级隔离实践
  • 赋能透明化生产:适配多介质微量喷涂的超声波流量传感器品牌推荐 - 品牌2026
  • WPF中利用ConverterParameter实现动态UI样式切换
  • LTspice新手必看:5分钟搞定无源低通滤波电路仿真(附完整参数计算)
  • 鸿蒙版瑞幸咖啡开发手记(二):首页UI与数据联动的工程实践
  • 避坑指南:用Fuzzy Logic Designer时容易忽略的5个细节(附曲面观测器调试技巧)
  • Qwen3-ASR-1.7B性能优化:提升语音识别速度的5个技巧
  • 浏览器中的JavaScript:从输入URL到代码执行的完整流程解析
  • 图解堆排序建堆过程:从{45,78,55}到初始堆的完整推导
  • 银河麒麟双网卡上网卡顿?3步搞定WIFI+有线同时稳定连接(附自动脚本)
  • 避坑指南:迪文串口屏文本显示常见问题及解决方案(含GBK编码设置)
  • GRPOConfig中num_generations参数详解:如何优化你的RLHF训练效率
  • Linux服务器性能调优必看:5种方法快速查询NUMA节点数量(附详细命令解析)
  • VSCode + CMake + MinGW:打造高效STM32 GCC开发环境
  • VSCode中Pandas绘图不显示?3步搞定IPython内核配置(附截图)
  • Windows 10/11下TeXLive 2021安装全攻略:从下载到第一个中文文档编译
  • Windows 10/11突然找不到WLAN?5种快速修复方法实测有效(附详细步骤)