位运算符
位运算符直接操作整数的二进制位,是 C 语言贴近硬件的体现。它们用于标志位管理、掩码操作、权限控制、数据压缩等底层场景。理解位运算,能让你高效地操控数据的每一位,也是阅读系统代码和硬件驱动的基础。
基本位运算符
| 运算符 | 名称 | 示例 | 说明 |
|---|---|---|---|
& | 按位与 | a & b | 两位都为 1,结果才为 1 |
| | 按位或 | a | b | 两位至少一个为 1,结果就为 1 |
^ | 按位异或 | a ^ b | 两位不同,结果为 1;相同为 0 |
~ | 按位取反 | ~a | 0 变 1,1 变 0 |
<< | 左移 | a << n | 所有位左移 n 位,右侧补 0 |
>> | 右移 | a >> n | 所有位右移 n 位,左侧补什么取决于类型 |
unsigned int a = 0b1010; /* 10 */
unsigned int b = 0b1100; /* 12 */
printf("%u\n", a & b); /* 1000 = 8 */
printf("%u\n", a | b); /* 1110 = 14 */
printf("%u\n", a ^ b); /* 0110 = 6 */
printf("%u\n", ~a); /* 按位取反,结果取决于 int 的位数 */
按位与 &
按位与常用于掩码操作——提取或保留特定的位:
unsigned int flags = 0b1101; /* 13 */
unsigned int mask = 0b0011; /* 3 */
unsigned int result = flags & mask; /* 0001 = 1 */
/* 检查第 2 位是否为 1 */
if (flags & 0b0100)
printf("Bit 2 is set\n");
/* 检查最低位 */
if (flags & 1)
printf("LSB is set\n");
按位或 |
按位或用于设置标志位:
unsigned int flags = 0;
flags = flags | 0b0001; /* 设置第 0 位 */
flags |= 0b0010; /* 设置第 1 位(复合赋值) */
flags |= 0b0100; /* 设置第 2 位 */
/* 现在 flags = 0b0111 = 7 */
按位异或 ^
异或的特性:相同为 0,不同为 1。一个有趣的性质是 a ^ a = 0,a ^ 0 = a:
unsigned int a = 0b1010;
printf("%u\n", a ^ a); /* 0 */
printf("%u\n", a ^ 0); /* 1010 = 10 */
/* 切换(翻转)特定位 */
unsigned int flags = 0b1100;
flags ^= 0b0100; /* 翻转第 2 位:1100 ^ 0100 = 1000 */
/* 交换两个变量(不使用临时变量)——不推荐实际使用 */
int x = 5, y = 3;
x = x ^ y;
y = x ^ y; /* y = (x^y)^y = x */
x = x ^ y; /* x = (x^y)^x = y */
异或交换在 x 和 y 指向同一内存时会失败(结果为 0),且现代编译器对普通交换的优化已经足够好,这种技巧没有实际优势。
按位取反 ~
~ 将所有位取反。注意:如果操作数是有符号整数,结果可能为负:
unsigned int u = 0;
printf("%u\n", ~u); /* 4294967295(32 位全 1) */
int s = 0;
printf("%d\n", ~s); /* -1(补码表示) */
移位运算
左移 <<:所有位左移,右侧补 0。左移 n 位等价于乘以 2ⁿ(对无溢出情况):
unsigned int a = 1;
printf("%u\n", a << 0); /* 1 */
printf("%u\n", a << 1); /* 2 */
printf("%u\n", a << 3); /* 8 */
printf("%u\n", a << 8); /* 256 */
/* 快速乘 2 */
unsigned int x = 10;
unsigned int doubled = x << 1; /* 20 */
左移溢出时,移出的位丢失:
unsigned int a = 0x80000000; /* 最高位为 1 */
printf("%u\n", a << 1); /* 0:最高位移出,变为 0 */
右移 >>:所有位右移。无符号数左侧补 0(逻辑右移);有符号数左侧补符号位还是补 0 是实现定义的(算术右移 vs 逻辑右移):
unsigned int u = 0xF0000000;
printf("%X\n", u >> 4); /* 0F000000:左侧补 0 */
int s = -16; /* 0xFFFFFFF0 */
printf("%d\n", s >> 2); /* -4:算术右移,补符号位 1 */
/* 或 1073741820:逻辑右移,补 0(实现定义) */
C99 §6.5.7/5 规定:有符号负数的右移是实现定义行为。为保证可移植性,需要算术右移时应使用无符号数的技巧,或确认目标平台的行为。
移位位数限制:
unsigned int a = 1;
unsigned int b = a << 32; /* 未定义行为!移位位数 >= 类型位数 */
unsigned int c = a << 31; /* 合法:最高位移到符号位 */
如果移位位数大于等于操作数的位数,或者为负数,结果是未定义行为。
实际应用
标志位管理:
#define FLAG_READ 0x01 /* 0001 */
#define FLAG_WRITE 0x02 /* 0010 */
#define FLAG_EXEC 0x04 /* 0100 */
unsigned int permissions = FLAG_READ | FLAG_WRITE; /* 设置读和写 */
if (permissions & FLAG_READ)
printf("Can read\n");
permissions &= ~FLAG_WRITE; /* 清除写权限 */
permissions ^= FLAG_EXEC; /* 切换执行权限 */
提取颜色分量:
unsigned int color = 0xFFAA55; /* RGB */
unsigned char r = (color >> 16) & 0xFF; /* 0xFF */
unsigned char g = (color >> 8) & 0xFF; /* 0xAA */
unsigned char b = color & 0xFF; /* 0x55 */
判断奇偶:
if (n & 1)
printf("Odd\n"); /* 最低位为 1 */
else
printf("Even\n"); /* 最低位为 0 */
常见错误
混淆逻辑与位运算符:
if (flags & FLAG_READ) /* 正确:位与检查标志 */
if (flags && FLAG_READ) /* 错误:逻辑与,flags 非零就真 */
对有符号数进行位运算:
int flags = 0x80;
if (flags >> 7) /* 实现定义:可能为 -1 或 1 */
/* ... */
/* 安全做法 */
unsigned int uflags = 0x80;
if (uflags >> 7) /* 总是 1 */
/* ... */
移位溢出:
int a = 1;
int b = a << 31; /* 有符号数:最高位为 1,变为负数 */
/* 如果 int 是 32 位,这是符号位 */
位运算应优先使用无符号类型,避免符号位带来的歧义。