文章目录
- 前言
- 一、左移操作(<<)和 右移操作(>>)
- 1.1 左移操作(<<)
- 1.2 右移操作(>>)
- 1.3 应用场景
- 二、按位与 (&) 和 按位或 (|)
- 2.1 按位与 (&)
- 2.2 按位或 (|)
- 2.3 按位操作的实际应用
- 三、应用案例
- 3.1 应用案例代码
- 3.2 应用案例分析
前言
本文主要探讨计算机中一些常见的运算,比如左移操作(<<)、右移操作(>>),按位与 (&) 、按位或 (|),最后通过一个例子结合按位与和右移操作实现获取一个字节中所有位的功能。
一、左移操作(<<)和 右移操作(>>)
左移和右移是计算机编程中常用的位操作,它们用于将数值的二进制位向左或向右移动,从而改变数值的大小。理解这些操作对处理低级别数据、优化代码性能以及执行特定算法(如加密和解密)都是非常有用的。
1.1 左移操作(<<)
左移操作符 << 会将一个数的所有二进制位向左移动指定的位数。右侧空出的位用 0 填充。左移操作实际上相当于乘以 2 的幂。
uint8_t a = 0b00001111; // 二进制表示,十进制 15
a = a << 2;
初始值:a = 0b00001111 (二进制 15)
执行 a << 2 后,a = 0b00111100 (二进制 60)
在这个例子中,a 的所有位向左移动了两位。从十进制的角度来看,左移两位相当于 15 * 2^2 = 60。即原来是15,左移两位后变成60。
数学意义:
从十进制的角度来看,左移 n 位,相当于乘以 2^n。例如:
x << 1 等同于 x * 2^1
x << 2 等同于 x * 2^2
以此类推…
这种位操作是非常高效的,因为在硬件层面上,位移操作比乘法更快。
1.2 右移操作(>>)
右移操作符 >> 会将一个数的所有二进制位向右移动指定的位数。左侧空出的位根据操作数是有符号的还是无符号的,以及编译器的实现,可能会填充 0 或符号位(即符号扩展)。
uint8_t b = 0b11000000; // 二进制表示,十进制 192
b = b >> 2;
初始值:b = 0b11000000 (二进制 192)
执行 b >> 2 后,b = 0b00110000 (二进制 48)
在这个例子中,b 的所有位向右移动了两位。从十进制的角度来看,右移两位相当于192 ÷ 2^2 = 48。即原来是192,右移两位后变成48。
数学意义:
从十进制的角度来看,右移 n 位,相当于除以 2^n。例如:
x >> 1 等同于 x ÷ 2^1
x >> 2 等同于 x ÷ 2^2
以此类推…
但是要注意,右移操作会截断数值。例如,5 >> 1 的结果是 2,而不是 2.5。
特别注意:
1. 无符号数和有符号数的区别:
- 对于无符号数,右移时左侧填充 0。
- 对于有符号数(如 int 类型的负数),右移时可能会进行符号扩展,即用原符号位填充左侧。例如,-2 的二进制为 11111110(假设 8 位),右移一位可能会变成 11111111(保持负数)。
2. 超出范围的移位:
- 如果移位的位数超过了数据类型的位数(例如,uint8_t 只有 8位),移位的结果是未定义的(依赖于具体的编译器实现),因此一般要确保移位数小于类型的位宽。
1.3 应用场景
场景 | 描述 |
---|---|
快速乘除法 | 左移和右移操作被广泛用于实现快速乘法和除法 |
位掩码操作 | 用于生成特定位的掩码,特别是在嵌入式系统中,常用来控制硬件 |
数据编码和解码 | 例如压缩算法、加密算法中需要处理位级数据 |
循环移位 | 一些应用中需要通过移位操作来实现循环移位,例如哈希函数的实现 |
二、按位与 (&) 和 按位或 (|)
位操作(如按位与和按位或)是一种在计算机系统中非常常见的运算,用于直接操作整数的二进制位。它们的工作原理是基于每个位的值,而不是对整个数字进行通常的数学运算,这对于底层硬件控制操作特别有用,因为硬件通常是通过具体的位进行控制的。
2.1 按位与 (&)
逻辑与的规则是:只要有一个位为 0
,结果就为 0
。只有在两个位都为 1
时,结果才为 1
。简而言之就是:有假为假,全真为真
位1 | 位2 | 结果 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
举例说明:
0b10101010 // 十进制 170
0b11001100 // 十进制 204
10001000// 十进制 136
170 & 204 = 136 即 0b10101010 & 0b11001100 = 0b10001000
按位与的逻辑是有假为假,全真为真,具体分析如下:
10101010// 十进制 170
11001100// 十进制 204
10001000// 十进制 136
使用计算器验证了一下没有问题
2.2 按位或 (|)
逻辑或的规则是:只要有一个位为 1
,结果就为 1
。只有在两个位都为 0
时,结果才为 0
。简而言之就是:有真为真,全假为假。
位1 | 位2 | 结果 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
举例说明:
0b10101010// 十进制 170
0b11001100// 十进制 204
0b11101110// 十进制 238
0b10101010 | 0b11001100 = 0b11101110
按位或的逻辑是有真为真,全假为假,具体分析如下:
10101010// 十进制 170
11001100// 十进制 204
11101110// 十进制 238
使用计算器验证了一下没有问题
2.3 按位操作的实际应用
通过按位与和按位或,可以有效地操作数据的特定位,达到如提取、设置、清除和切换等目的。
1. 提取特定位
按位与操作常用于从一个字节或一个数中提取特定的位,以下代码实现了在一个字节中依次提取位的操作(从左(最高位)往右(最低位))
typedef enum {
Bit_RESET = 0,
Bit_SET
} BitAction;
void MyI2C_W_SDA(int BitValue) {
printf("BitValue:%d\n", BitValue);
}
void MyI2C_SendByte(unsigned char Byte) {
int i;
for (i = 0; i < 8; i++) {
// 使用三元运算符将结果转换为 0 或 1
MyI2C_W_SDA((Byte & (0x80 >> i)) ? Bit_SET : Bit_RESET);
}
}
首先,按照上面分析可知,若想从一个字节中提取特定的位(不是0就是1),我们可以使用按位与的思想。先从最高位开始,也就是0b10000000,转换成十六进制即0x80,然后我们让0x80依次右移即可。
假设 Byte = 0x75,转换成二进制为0b01110101,那么具体的过程如下:
第一次循环 (i = 0):
0x80 >> 0 = 10000000
01110101 & 10000000 = 00000000(零值)
零值 ? Bit_SET : Bit_RESET(Bit_RESET)
第二次循环 (i = 1):
0x80 >> 1 = 01000000
01110101& 01000000 = 01000000(非零值)
非零值 ? Bit_SET : Bit_RESET(Bit_SET)
第三次循环 (i = 2):
0x80 >> 1 = 00100000
01110101& 00100000= 00100000 (非零值)
非零值 ? Bit_SET : Bit_RESET(Bit_SET)
…
如此循环8次即可完成一个字节所有位的获取。
2. 设置特定位
按位或操作通常用于设置某些特定位为1,如下所示为设置number的第2位为1。
uint8_t number = 0b10100000;
现在我想要设置number的第2位为1(高位在左,低位在右)
number = number | 0b00000100; // 设置第2位为1
number = 0b10100100; // 二进制,十进制为 164
三、应用案例
3.1 应用案例代码
main.c:
#include <stdio.h>
#define MPU6050_PWR_MGMT_1 0x6B // 0110 1011
#define MPU6050_PWR_MGMT_2 0x6C // 0110 1100
#define MPU6050_WHO_AM_I 0x75 // 0111 0101
typedef enum {
Bit_RESET = 0,
Bit_SET
} BitAction;
void MyI2C_W_SDA(int BitValue) {
printf("BitValue:%d\n", BitValue);
}
void MyI2C_SendByte(unsigned char Byte) {
int i;
for (i = 0; i < 8; i++) {
// 使用三元运算符将结果转换为 0 或 1
MyI2C_W_SDA((Byte & (0x80 >> i)) ? Bit_SET : Bit_RESET);
}
}
int main()
{
printf("MPU6050_WHO_AM_I,0x75 ---> 0111 0101\n");
MyI2C_SendByte(MPU6050_WHO_AM_I);
printf("MPU6050_PWR_MGMT_1,0x6B ---> 0110 1011\n");
MyI2C_SendByte(MPU6050_PWR_MGMT_1);
printf("MPU6050_PWR_MGMT_2,0x6C ---> 0110 1100\n");
MyI2C_SendByte(MPU6050_PWR_MGMT_2);
return 0;
}
输出结果如下:
jeff@jeff:/tmp$ ./main
MPU6050_WHO_AM_I,0x75 ---> 0111 0101
BitValue:0
BitValue:1
BitValue:1
BitValue:1
BitValue:0
BitValue:1
BitValue:0
BitValue:1
MPU6050_PWR_MGMT_1,0x6B ---> 0110 1011
BitValue:0
BitValue:1
BitValue:1
BitValue:0
BitValue:1
BitValue:0
BitValue:1
BitValue:1
MPU6050_PWR_MGMT_2,0x6C ---> 0110 1100
BitValue:0
BitValue:1
BitValue:1
BitValue:0
BitValue:1
BitValue:1
BitValue:0
BitValue:0
jeff@jeff:/tmp$
3.2 应用案例分析
相信通过上面的分析后大家对按位与按位或以及左移右移的操作已经心中有数了,这里我就不再累述了,在这里我只讲两点注意事项:
第一点,执行完 Byte & (0x80 >> i) 运算后结果仍然是一个8位1个字节的数据,只不过这个数据只有两个结果,0或者是非0。如果目标数据当前位为1,那么结果就是非0,如果目标数据当前位为0,那么结果就是0。可以通过三元运算的方式将0和非0转换成0和1。
第二点,三元运算符的语法是:
condition ? true_value : false_value;
condition 是一个表达式,计算后返回一个布尔值(trur or 1 或 false or 0)
如果 condition 为true or 1,那么整个表达式的结果是 true_value
如果 condition 为 false or 0,那么整个表达式的结果是 false_value
对应我们这里即,0就是Bit_RESET,1就是Bit_SET。
typedef enum {
Bit_RESET = 0,
Bit_SET
} BitAction;
MyI2C_W_SDA((Byte & (0x80 >> i)) ? Bit_SET : Bit_RESET);