C语言——位操作运算
位操作是 C 语言中直接对二进制位进行运算的底层操作,在系统编程、嵌入式开发、算法优化等领域有重要应用。C语言提供了 6 个位操作运算符,这些运算符只能作用于整型操作数,也就是只能作用于带符号或无符号 char、short、int、long 类型。以下是六种操作符及其表示方法:
含义 | 表示符号 |
按位与(AND) | & |
按位或(OR) | | |
按位异或(XOR) | ^ |
按位取反(一元运算符) | ~ |
左移 | << |
右移 | >> |
由于 C 语言中 printf 没有输出二进制形式数据的格式,所以首先自定义一个打印二进制形式数据的函数print_binary。其示例如下:
#include <stdio.h>
#include <stdint.h>
/**
* @brief 二进制输出函数
* @param num: 需要转换的数据
* @param bits:转换后二进制的位数
*/
void print_binary(uint32_t num, int bits)
{
for (int i = bits - 1; i >= 0; i--) {
printf("%d", (num >> i) & 1); /* 移位 并和 1相与 得到对应 位 */
if (i && (i % 4 == 0)) { /* 每 4 位加一个空格 */
printf(" ");
}
}
printf("\n");
}
int main()
{
/* 正数在计算机中是以二进制补码形式存储,正数的原码、反码和补码一致 */
print_binary(18, 8); /* 输出 8 位: 0001 0010 */
/* 负数在计算机中是以二进制补码的形式存储的 */
print_binary(-18, 8); /* 1110 1110 -> -18的补码形式,原码为 1001 0010 */
return 0;
}
还有一点,要了解有符号数在计算机中存储形式, 在计算机中数值一律以二进制补码的形式储存。
- 正数的原码、反码及补码的形式是一样的。
- 但是负数的原码、反码及补码的形式不一样,负数的二进制原码的最高位是符号位,假设现在令 X = -13,则其二进制原码形式为 1000 1101。
- 负数的二进制反码是由二进制原码的符号位不变,其余各位取反所得。则 X 的二进制反码形式为 1111 0010。
- 负数的二进制补码则为二进制反码符号位不变再在最末位加上 1 所得。则 X 的二进制补码形式为 1111 0011。
1、基础位操作符
1.1、按位与 &
- 特性:对应位同为 1 时结果为 1,否则为 0。
- 示例:
uint8_t a = 0b00011010, b = 0b01001100, c = 0b000000000;
c = a & b;
print_binary(c, 8); /* 输出:0000 1000 */
- 应用:
1、掩码操作:提取特定位。
uint8_t port = 0b11001010;
uint8_t masked = port & 0x0F; /* 获取低四位 -> 0b00001010 */
print_binary(masked, 8); /* 输出:0000 1010 */
2、清零位:x &= ~(1 << n)
uint8_t x = 0b11100100;
x = x & ~(1 << 6); /* 将 x 的bit6清零 -> 0b10100100 */
print_binary(x, 8); /* 输出:1010 0100 */
1.2、按位或 |
-
特性:对应位的任意一位为 1 则结果为 1。
-
示例:
uint8_t a = 0b01000010, b = 0b10001010, c;
c = a | b;
print_binary(c, 8); /* 输出:1100 1010 */
- 应用:
1、设置位:x |= (1 << n)
uint8_t x = 0b00001010;
x = x | (1 << 5); /* 设置 x 的bit5 -> 0b00011010 */
print_binary(x, 8); /* 输出:0010 1010 */
2、合并标志位
#define READ 0x01 /* 0000 0001 */
#define WRITE 0x02 /* 0000 0010 */
int flags = READ | WRITE;
print_binary(flags, 8); /* 0000 0011 */
printf("0x%02x\n", flags); /* 0x03 */
1.3、按位异或 ^
- 特性:对应位相同为 0,不同为 1。
- 示例:
uint8_t a = 0b01000010, b = 0b10001010, c;
c = a ^ b;
print_binary(c, 8); /* 输出:1100 1000 */
- 应用:
1、交换变量(不需要使用临时变量)。
int m = 5, n = 3;
m ^= n;
n ^= m;
m ^= n;
printf("m = %d, n = %d\n", m, n); /* m = 3, n = 5 */
2、数据加密(使用密钥异或)。
3、切换位状态:x ^= (1 << n)
uint8_t x = 0b11001010;
x ^= (1 << 6); /* 切换 x 的bit6 -> 0b10001010 */
print_binary(x, 8); /* 输出:1000 1010 */
1.4、按位取反 ~
- 特性:将数据二进制形式的各位反转,0 变为 1, 1 变为 0。
- 注意:结果与数据类型的宽度有关。
- 示例:
uint8_t q = 0b11000100;
print_binary(~q, 8); /* 0011 1011 */
- 应用:
uint8_t mask = ~0x07;
print_binary(mask, 8);
1.5、移位操作
1.5.1、左移 <<
- 特性:低位补 0,相当于乘 2^n。
int d = 5;
print_binary(d, 8); /* 0000 0101 */
print_binary(d << 3, 8); /* 0010 1000 */
printf("d = %d\n", d << 3); /* 40 */
1.5.2、右移 >>
-
特性:无符号数,高位补 0(逻辑移位);有符号数,补符号位(算术移位)。
在计算机内存当中,负数一律按照补码的形式进行存储,例如现在有一个负数 -8,其二进制原码形式为,这个地方需要注意的是高位的 1 为符号位,即当这个数字是负数的时候高位为 1,正数时高位为 0,且符号为不计入数值当中,只能表示正负数的概念。
当 -8 存入计算机当中的时候,内存中需要对负数的原码符号位不变再进行按位取反加一的操作,即进行求补码的操作;特别注意,符号位不参与变化,补码为。负数右移操作时需要补符号位 1 ,则右移 2 位后为
。
当需要将移位后的负数从内存当中取出的时候,首先需要将补码转化成原码,转变规则为对当前的补码取反加一(其中符号位不参与变化),转变后的原码为:。
1.5.3、应用
-
快速计算:x * 8 --> x << 3
-
提取位字段:(x >> 4) & 0x0F
2、高级应用技巧
2.1、位掩码技术
- 检查位:
if (x & (1 << n))
- 设置多个位:
x |= (BIT0 | BIT2)
- 清除多个位:
x &= ~(BIT1 | BIT3)
2.2、位字段结构
struct {
unsigned int enable : 1;
unsigned int mode : 3;
unsigned int status : 4;
} device_reg;
2.3、高效位计数
int count_bits(uint32_t x)
{
int count = 0;
while (x) {
x &= x - 1; /* 清除最低位的 1 */
count++;
}
return count;
}
2.4、奇偶校验
bool is_odd(uint8_t x)
{
return x & 1; /* 比 x % 2 更快 */
}
3、典型应用场景
3.1、硬件寄存器操作
/* 设置 GPIO 引脚为输出模式 */
#define GPIO_MODE_OUT (1 << 0)
volatile uint32_t *reg = (uint32_t*)0x40020000;
*reg |= GPIO_MODE_OUT; /* 设置位 */
*reg &= ~(0X0F << 4); /* 清除高 4 位 */
3.2、数据压缩存储
/* 存储 8 个 bool 值到 1 字节 */
uint8_t flags = 0;
flags |= (condition1 << 0);
flags |= (condition2 << 1);
/* 读取第 3 位 */
bool val = (flags >> 2) & 1;
3.3、图像处理
/* 快速计算 RGB 平均值 */
uint32_t pixel = 0xRRGGBB; /* 此处RR、GG、BB仅是代表RGB通道的值 */
uint8_t avg = ((pixel >> 16) + ((pixel >> 8) & 0xFF) + (pixel & 0xFF)) / 3;
4、性能优化示例
传统方法:
bool is_odd(uint8_t x)
{
if (x % 2 == 0)
return false;
else
return true;
}
位操作优化:
bool is_odd(uint8_t x)
{
return x & 1;
}
掌握位操作可以显著提升底层系统编程能力,但在应用时需权衡效率与代码可读性。建议:关键位置使用位操作优化,复杂逻辑优先保证代码可维护性。
5、注意事项
5.1、移位安全
-
避免超出类型宽度,假设 uint32_t x,这时 x 是32位数据,如果 x << 33 则是未定义行为。
#include <stdio.h>
#include <stdint.h>
int main()
{
uint32_t x = 0xFFFF; /* 数据只有 32 位 */
uint32_t y = x << 33; /* 左移 33 位导致算术溢出 */
printf("%d\n", y); /* 131070 */
return 0;
}
- 有符号数右移结果与编译器的实现相关。
#include <stdio.h>
int main()
{
int x = -15;
printf("(-15 >> 3) = %d\n", x >> 3); /* -2 */
print_binary(x, 8); /* 1111 0001 -> -15的补码形式,原码为 1000 1111 */
print_binary(x >> 3, 8); /* 1111 1110 -> 15右移2位的补码,原码为 1000 0010 */
return 0;
}
5.2、运算符优先级
位操作符优先级低于比较运算符,建议多使用括号。
if (x & 0x0F == 0x08) /* 错误:实际是 x & (0x0F == 0x08)
/* 建议写法 */
if ((x & 0x0F) == 0x08)
5.3、可移植性
- 字节序(大小端)问题影响位字段的内存布局。
- 使用固定宽度类型(如uint_t)增强可移植性。