C语言位运算

计算机内部以二进制形式存储数据,位运算(Bitwise Operations)直接对整数的二进制位进行操作,是C语言底层编程的重要特性。 位运算具有执行速度快、代码紧凑的优点,广泛应用于嵌入式系统、通信协议、图形处理、权限控制、数据加密等领域。 本章将详细讲解C语言的六种位运算符及其典型应用场景。

⚙️ 位运算符概述

C语言提供了六种位运算符,它们只能用于整型操作数(char、short、int、long等,包括有符号和无符号)。

3232 3232 3232 3232 3232 3232 3232
运算符名称说明
&按位与两个位都为1时结果为1,否则为0|按位或两个位至少有一个为1时结果为1,否则为0^按位异或两个位不同时结果为1,相同时为0~按位取反单目运算符,将每个位取反(0变1,1变0)<<左移将二进制位向左移动指定的位数,右侧补0>>右移将二进制位向右移动指定的位数,无符号数高位补0,有符号数高位补符号位(依赖实现)

🔗 按位与(&)

按位与常用于掩码操作,保留某些位,清零其他位。

#include <stdio.h>

int main() {
    unsigned char a = 0b10101100;  // 172
    unsigned char b = 0b01101101;  // 109
    unsigned char c = a & b;       // 00101100 (44)

    printf("a & b = %u\n", c);

    // 应用:提取低4位
    unsigned char x = 0xAB;  // 10101011
    unsigned char low = x & 0x0F;  // 00001011 (11)
    printf("低4位: %u\n", low);

    return 0;
}

🔗 按位或(|)

按位或用于设置某些位为1,而不影响其他位。

#include <stdio.h>

int main() {
    unsigned char a = 0b10101100;  // 172
    unsigned char b = 0b01101101;  // 109
    unsigned char c = a | b;       // 11101101 (237)

    printf("a | b = %u\n", c);

    // 应用:设置第3位为1(从0开始计数)
    unsigned char x = 0b10101010;  // 170
    unsigned char set = x | (1 << 3);  // 10111010 (186)
    printf("设置后: %u\n", set);

    return 0;
}

🔁 按位异或(^)

按位异或用于翻转特定位,也可用于交换两个变量而不使用临时变量。

#include <stdio.h>

int main() {
    unsigned char a = 0b10101100;  // 172
    unsigned char b = 0b01101101;  // 109
    unsigned char c = a ^ b;       // 11000001 (193)

    printf("a ^ b = %u\n", c);

    // 应用:翻转第2位
    unsigned char x = 0b10101010;  // 170
    unsigned char flip = x ^ (1 << 2);  // 10101110 (174)
    printf("翻转后: %u\n", flip);

    // 交换两个整数(不使用临时变量)
    int m = 10, n = 20;
    m = m ^ n;
    n = m ^ n;
    m = m ^ n;
    printf("交换后: m=%d, n=%d\n", m, n);

    return 0;
}
异或的性质: a ^ a = 0,a ^ 0 = a,且满足交换律和结合律,常用于加密和解密。

🔁 按位取反(~)

按位取反将操作数的所有位取反,包括符号位。通常用于获得补码。

#include <stdio.h>

int main() {
    unsigned char a = 0b10101100;  // 172
    unsigned char b = ~a;          // 01010011 (83)

    printf("~a = %u\n", b);

    // 有符号整数的取反(注意:~a = -(a+1))
    int x = 5;
    int y = ~x;  // -6
    printf("~5 = %d\n", y);

    return 0;
}

⬅️ 左移(<<)

左移将二进制位向左移动指定的位数,右侧补0。左移n位相当于乘以2的n次方(不溢出时)。

#include <stdio.h>

int main() {
    unsigned char a = 0b00001010;  // 10
    unsigned char b = a << 2;      // 00101000 (40)

    printf("10 << 2 = %u\n", b);

    // 快速乘以2
    int x = 7;
    int y = x << 1;  // 14
    printf("7 * 2 = %d\n", y);

    return 0;
}
注意: 左移可能溢出,且对有符号数左移时符号位可能被改变,结果由实现定义。通常建议对无符号数进行位运算。

➡️ 右移(>>)

右移将二进制位向右移动指定的位数。对于无符号数,高位补0(逻辑右移);对于有符号数,高位通常补符号位(算术右移),但具体行为依赖编译器实现。

#include <stdio.h>

int main() {
    unsigned char a = 0b10101000;  // 168
    unsigned char b = a >> 2;      // 00101010 (42)

    printf("168 >> 2 = %u\n", b);

    // 快速除以2(整数除法)
    int x = 15;
    int y = x >> 1;  // 7(向下取整)
    printf("15 / 2 = %d\n", y);

    // 有符号数右移(算术右移)
    int signedX = -8;  // 二进制补码表示 ...11111000
    int signedY = signedX >> 2;  // 通常结果为 -2
    printf("-8 >> 2 = %d\n", signedY);

    return 0;
}
建议: 为了保证可移植性,对于位运算,尽量使用 unsigned 类型。

💡 位运算应用实例

🔸 位掩码与权限控制

使用位掩码可以紧凑地表示多个布尔标志。

#include <stdio.h>

// 权限标志
#define PERM_READ   0x01  // 0001
#define PERM_WRITE  0x02  // 0010
#define PERM_EXEC   0x04  // 0100
#define PERM_DELETE 0x08  // 1000

int main() {
    unsigned char permissions = 0;

    // 添加读和写权限
    permissions |= PERM_READ | PERM_WRITE;

    // 检查是否有读权限
    if (permissions & PERM_READ) {
        printf("具有读权限\n");
    }

    // 移除写权限
    permissions &= ~PERM_WRITE;

    // 检查写权限
    if (!(permissions & PERM_WRITE)) {
        printf("写权限已被移除\n");
    }

    // 切换执行权限(有则删,无则加)
    permissions ^= PERM_EXEC;

    return 0;
}

🔸 判断奇偶性

int isOdd(int n) {
    return n & 1;   // 最低位为1则为奇数
}

🔸 计算2的幂次

int powerOfTwo(int n) {
    return 1 << n;   // 2^n
}

🔸 获取某一位的值

int getBit(int num, int pos) {
    return (num >> pos) & 1;
}

🔸 设置某一位为1

int setBit(int num, int pos) {
    return num | (1 << pos);
}

🔸 清除某一位(置0)

int clearBit(int num, int pos) {
    return num & ~(1 << pos);
}

🔸 交换两个整数(不使用临时变量)

void swap(int *a, int *b) {
    *a = *a ^ *b;
    *b = *a ^ *b;
    *a = *a ^ *b;
}

🔸 快速乘除2的幂次

int multiplyByPowerOfTwo(int x, int n) {
    return x << n;   // x * 2^n
}

int divideByPowerOfTwo(int x, int n) {
    return x >> n;   // x / 2^n(向下取整)
}

🔸 计算绝对值(不包含分支)

int absValue(int x) {
    int mask = x >> (sizeof(int) * 8 - 1);  // 符号位填充
    return (x + mask) ^ mask;
}

📋 综合示例:IP地址解析与构造

#include <stdio.h>

// 将IP字符串转换为32位整数(网络字节序)
unsigned int ipToInt(const char *ip) {
    unsigned int a, b, c, d;
    sscanf(ip, "%u.%u.%u.%u", &a, &b, &c, &d);
    return (a << 24) | (b << 16) | (c << 8) | d;
}

// 将32位整数转换为IP字符串
void intToIp(unsigned int ip, char *buffer) {
    sprintf(buffer, "%u.%u.%u.%u",
            (ip >> 24) & 0xFF,
            (ip >> 16) & 0xFF,
            (ip >> 8) & 0xFF,
            ip & 0xFF);
}

int main() {
    char ipStr[] = "192.168.1.100";
    unsigned int ipInt = ipToInt(ipStr);
    printf("%s 转换为整数: %u (0x%X)\n", ipStr, ipInt, ipInt);

    char buffer[16];
    intToIp(ipInt, buffer);
    printf("整数 0x%X 转换回IP: %s\n", ipInt, buffer);

    // 应用:获取网络号(假设子网掩码255.255.255.0)
    unsigned int mask = 0xFFFFFF00;
    unsigned int network = ipInt & mask;
    printf("网络号: ");
    intToIp(network, buffer);
    printf("%s\n", buffer);

    return 0;
}

⚠️ 常见错误与注意事项

  • 混淆逻辑与位运算&&|| 是逻辑运算符,&| 是位运算符,不要混淆。
  • 右移对有符号数的行为不确定:有符号整数右移可能补符号位(算术右移)或补0(逻辑右移),取决于编译器。应优先使用 unsigned 类型。
  • 左移超出类型范围:左移超过位宽会导致溢出,行为由实现定义。例如 1 << 32 在32位系统上结果不确定。
  • 位运算优先级较低:例如 a & b == c 等价于 a & (b == c),应使用括号明确优先级。
  • 对浮点数进行位运算:不允许直接对浮点数进行位运算,但可以通过联合体或指针转换,不过会破坏可移植性。
  • 误用按位取反产生意料之外的值~0 在32位系统中是 0xFFFFFFFF,但若赋给有符号数,值为-1。要理解补码表示。
  • 忽略类型大小:在不同平台上 int 的宽度可能不同(16位、32位、64位),位运算时应考虑可移植性。

位运算是C语言高效编程的精髓之一,在系统编程、嵌入式开发、性能敏感场景中不可或缺。 掌握位运算,能让你编写出更紧凑、更底层的代码。后续的高级专题中,我们将进一步探讨位运算在数据结构与算法中的巧妙应用。