一、位运算概述
在计算机系统中,数据最终都以二进制 形式存储。无论是一个普通整型变量,还是一个寄存器值,处理器看到的本质都是由若干个 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 = 00 & 1 = 01 & 0 = 01 & 1 = 1 复制代码 下面举个具体示例来进行说明:uint8_t a = 0b1101;uint8_t b = 0b1011;uint8_t c = a & b; 复制代码 对 a 与 b 逐位对比:a: 1 1 0 1b: 1 0 1 1a&b: 1 0 0 1 复制代码 所以最终 c 的结果就是 0b1001,这就是按位与的执行逻辑。在实际应用中,按位与最典型的用途不是“求值”,而是屏蔽不关心的位 ,也就是后面常说的 mask 操作。
(三)按位或 |
运算规则:对应位只要有一个是1,结果就是1 。0 | 0 = 00 | 1 = 11 | 0 = 11 | 1 = 1 复制代码 继续按照按位与中的示例来进行说明:a: 1 1 0 1b: 1 0 1 1a|b: 1 1 1 1 复制代码 通过按位或运算后,最终 c 的结果变为了 0b1111,在实际使用中,我们也经常通过按位或去达到对某一bit进行置位的目的
(四)按位异或 ^
运算规则:对应位不同为1,相同为0 。0 ^ 0 = 00 ^ 1 = 11 ^ 0 = 11 ^ 1 = 0 复制代码 继续按照按位与中的示例进行说明:a: 1 1 0 1b: 1 0 1 1a^b: 0 1 1 0 复制代码 通过按位异或运算后,最终 c 的结果变成了 0b0110。所以对按位异或可以总结出以下通式://x表示任意整数变量x ^ 0 = xx ^ x = 0 复制代码 (五)按位取反 ~
运算规则:每一位都翻转 。1 = 0000 0001~1 = 1111 11100 = 0000 0000~0 = 1111 1111 复制代码 从这个示例可以看出,对 1 或 0 进行 ~ 时是针对其数据类型的整个类型宽度 进行取反的,而不是只关心显式的那几位bit,比如://uint8_t1 = 0000 0001~1 = 1111 11100 = 0000 0000~0 = 1111 1111//uint16_t1 = 0000 0000 0000 0001~1 = 1111 1111 1111 11100 = 0000 0000 0000 0000~0 = 1111 1111 1111 1111 复制代码 (六)左移 </h2p 运算规则:strong将所有 bit 整体向左移动 n 位,高位溢出的bit丢弃,低位补0/strong。/p1010 1010 < 1 = 0101 0100 复制代码 p 对于无符号数,在不溢出的前提下:/pp 比如:/p5 < 1 = 105 < 2 = 205 < 3 = 40 复制代码 p 这个并不是所有场景都可以将左移等同乘法,只要发生溢出,高位被丢弃,结果就不再等价。比如:/p//二进制1010 1010 = 十进制1701010 1010 < 1/* 按照左移乘法的公式来算,8位整数只能表示0~255, 所以但从范围边界来看,移位后的结果也一定发生 了截断,即大于0小于255*/170 * 2 = 340/* 移位后可以得到0101 0100,再转 十进制84,而84与340不等,所以若 发生溢出就无法再使用去做乘法。*/1010 1010 < 1 = 0101 0100 复制代码 p 而在C标准中,strong如果有符号数左移后溢出导致结果不能表示,就会产生未定义行为/strong。所以strong在实际工程中,一般更好是对 unsigned 类型的数据进行左移操作/strong。/ph2(七)右移 /h2p 运算规则:strong将所有 bit 整体向右移动 n 位,低位移除的 bit 被丢弃/strong。 /pp 可以看到,0000 1101 最右边的 1 被移出丢弃后变成了 0000 0110。/pp 对于无符号数:/pp 需要注意右移与左移不同,右移有两种情况,一种是逻辑右移,一种是算术右移,而左移则只有一种情况,并没有做区分。那么为什么右移会出现这两种情况呢,主要还是因为右移涉及到了有符号整形中的符号变化问题。/pp 在strong无符号数(unsigned)/strong中,右移整形只需要在strong高位补0/strong即可,比如/punsigned char a = 0b10101010;a 1 = 0b0101 0101; 复制代码 p 这是strong逻辑右移/strong。/pp 在strong有符号(signed)/strong中,右移整形大多数编译器会在strong高位补符号位/strong,比如:/p1110 0000 1//若是负数,高位补1= 1111 0000//若是正数,高位补0= 0111 0000 复制代码 p 这是strong算术右移/strong。/pp 针对左移和右移可做如下总结:/ptabletheadtrth运算/thth补位/thth特殊情况/th/tr/theadtbodytrtd/tdtd补0/tdtd可能溢出/td/trtrtd unsigned/tdtd补0/tdtd逻辑右移/td/trtrtd>> signed[/td][td]补符号位[/td][td]算术右移[/td][/tr][/table] 补充一句:在补码系统中,左移相当于乘2,右移相当于除2,但只有在不发生溢出且使用算术右移时才能保持符号正确。所以在实际开发中,更推荐使用无符号数(unsigned)进行位运算。
三、位运算常用操作
这一部分时位运算在工程中最常用的四类基本动作:置位、清零、读取、翻转 。
(一)置位
在对寄存器进行操作时,我们通常需要将寄存器的第 n 个 bit 置1 ,此时我们可以
[code]reg |= (1U
相关推荐