C语言:位运算实战

前言:

位运算是嵌入式底层、硬件驱动、网络协议、笔试算法的核心刚需能力,直接操作二进制比特位,执行效率远高于普通算术运算,是底层开发者的必备技能。本篇从基础运算符、工程常用技巧,到硬件寄存器操作、大小端处理与笔试高频手撕题全覆盖,全部为工业界真实落地用法,兼顾面试考点与工程实战价值。


一、六大基础位运算符核心用法

1. 按位与 &

运算规则:对应二进制位同为 1,结果才为 1,否则为 0。

工程核心用途

  • 清零指定比特位:和 0 相与即可清零

  • 保留指定比特位:和 1 相与即可保留

  • 判断奇偶:x & 1 结果为 1 则是奇数,0 则是偶数,比取模运算效率更高

    // 判断奇偶
    int is_odd(int x) {
    return x & 1;
    }

    // 保留低8位,其余位清零
    uint32_t keep_low8(uint32_t val) {
    return val & 0xFF;
    }

2. 按位或 |

运算规则:对应二进制位有一个为 1,结果就为 1。

工程核心用途

  • 置 1 指定比特位:和 1 相或即可置 1

  • 合并多个标志位:多个状态位按位或组合成一个状态字

    // 将第3位(从0开始计数)置1,不影响其他位
    uint32_t set_bit3(uint32_t val) {
    return val | (1 << 3);
    }

3. 按位异或 ^

运算规则:对应二进制位不同为 1,相同为 0。

核心特性

  • 任何数和自身异或结果为 0:x ^ x = 0

  • 任何数和 0 异或结果为自身:x ^ 0 = x

  • 满足交换律和结合律 工程核心用途

  • 翻转指定比特位:和 1 异或翻转,和 0 异或不变

  • 交换两个变量(无需临时变量)

  • 加密、校验算法基础

    // 不用临时变量交换两个整数
    void swap(int *a, int *b) {
    *a = *a ^ *b;
    *b = *a ^ *b;
    *a = *a ^ *b;
    }

4. 按位取反~

运算规则:所有二进制位 0 变 1,1 变 0。

工程核心用途:配合与运算实现指定位置零,写法更简洁。

复制代码
// 将第3位清零,其他位不变
uint32_t clear_bit3(uint32_t val) {
    return val & ~(1 << 3);
}

注意:~是单目运算符,对整个数据类型所有位取反,操作有符号数时需注意符号位变化。

5. 左移 <

运算规则:二进制位整体左移 n 位,高位丢弃,低位补 0。

数学意义:无符号数左移 n 位等价于乘以 2 的 n 次方,效率远高于乘法。

工程用途:生成位掩码、数值快速乘 2。

6. 右移 >>

运算规则:二进制位整体右移 n 位。

  • 无符号数:逻辑右移,高位补 0
  • 有符号数:算术右移,高位补符号位(正数补 0,负数补 1)

数学意义:正数右移 n 位等价于除以 2 的 n 次方,向下取整。

工程用途:快速除 2、提取高位字段。


二、位掩码工程封装技巧

位掩码是硬件驱动、协议开发的最常用手法,通过掩码提取、修改寄存器中的特定字段,是嵌入式开发的基础编码能力。

1. 位操作通用宏封装

工业界标准写法,通过宏封装通用的位操作,代码可读性强、不易出错:

复制代码
// 置位:将val的第n位(从0开始)置1
#define SET_BIT(val, n)    ((val) |= (1U << (n)))

// 清位:将val的第n位清零
#define CLEAR_BIT(val, n)  ((val) &= ~(1U << (n)))

// 翻转位:将val的第n位取反
#define TOGGLE_BIT(val, n) ((val) ^= (1U << (n)))

// 读位:读取val的第n位的值,返回0或1
#define READ_BIT(val, n)   (((val) >> (n)) & 1U)

规范细节:使用1U无符号整数,避免有符号数左移溢出的未定义行为。

2. 多段位操作(寄存器字段)

硬件寄存器通常按字段划分,一个寄存器包含多个功能位段,需要提取、修改指定段的值。

复制代码
// 提取位段:从val中提取从offset位开始、长度为len的字段值
#define GET_FIELD(val, offset, len) \
    (((val) >> (offset)) & ((1U << (len)) - 1U))

// 设置位段:将val的offset位开始的len位,设置为field_val
#define SET_FIELD(val, offset, len, field_val) \
    do { \
        (val) &= ~(((1U << (len)) - 1U) << (offset)); \
        (val) |= ((field_val) & ((1U << (len)) - 1U)) << (offset); \
    } while(0)

使用示例

复制代码
// 假设寄存器bit3~bit6是4位的波特率配置字段
uint32_t reg = 0;
SET_FIELD(reg, 3, 4, 0x0A); // 设置波特率字段为10
uint32_t baud = GET_FIELD(reg, 3, 4); // 读取波特率字段值

3. 标志位组合技巧

系统中多个布尔状态可以合并到一个整型变量中,大幅节省内存,是嵌入式资源受限场景的常用手法。

复制代码
// 定义状态标志位
#define FLAG_POWER_ON   (1 << 0)
#define FLAG_TX_READY   (1 << 1)
#define FLAG_RX_DONE    (1 << 2)
#define FLAG_ERROR      (1 << 3)

uint8_t system_status = 0;

// 置位多个标志
system_status |= FLAG_POWER_ON | FLAG_TX_READY;

// 判断是否同时满足多个条件
if ((system_status & (FLAG_POWER_ON | FLAG_TX_READY)) == (FLAG_POWER_ON | FLAG_TX_READY)) {
    // 上电完成且发送就绪,执行发送
}

// 清除标志
system_status &= ~FLAG_ERROR;

三、大小端原理与处理方案

大小端是数据存储的字节序问题,是跨平台通信、网络协议、嵌入式开发的必考点,也是笔试高频面试题。

1. 核心概念

  • 大端模式(Big Endian):数据的高字节存放在低地址,低字节存放在高地址,符合人类阅读习惯。网络字节序默认是大端。
  • 小端模式(Little Endian):数据的低字节存放在低地址,高字节存放在高地址。x86、ARM 默认都是小端模式。

0x12345678存放在地址 0x1000 为例:

地址 大端模式 小端模式
0x1000 0x12(高字节) 0x78(低字节)
0x1001 0x34 0x56
0x1002 0x56 0x34
0x1003 0x78(低字节) 0x12(高字节)

2. 手撕题:判断机器大小端

方法一:联合体法(最常用,面试首选)

利用联合体所有成员共享同一块内存的特性,通过赋值后读取第一个字节判断。

复制代码
int is_little_endian(void) {
    union {
        int a;
        char b;
    } u;
    u.a = 1;
    return u.b; // 小端返回1,大端返回0
}
方法二:指针法

通过强制类型转换,读取整型第一个字节判断。

复制代码
int is_little_endian(void) {
    int a = 1;
    char *p = (char *)&a;
    return *p; // 第一个字节是1则为小端
}

3. 大小端转换常用宏

网络通信、跨平台数据交互时,需要进行主机字节序和网络字节序的转换,标准库提供了相关函数,底层本质是移位与位运算。

复制代码
// 16位数据字节序翻转
uint16_t swap16(uint16_t val) {
    return (val >> 8) | (val << 8);
}

// 32位数据字节序翻转
uint32_t swap32(uint32_t val) {
    return  ((val & 0xFF000000) >> 24) |
            ((val & 0x00FF0000) >> 8)  |
            ((val & 0x0000FF00) << 8)  |
            ((val & 0x000000FF) << 24);
}

四、笔试高频位运算算法题

1. 二进制中 1 的个数(剑指 Offer 原题)

题目:输入一个整数,输出该数二进制表示中 1 的个数。

最优解法:消去最低位的 1 核心技巧:n & (n-1) 会消去 n 二进制中最右边的一个 1,有多少个 1 就执行多少次。

复制代码
int hammingWeight(uint32_t n) {
    int count = 0;
    while (n != 0) {
        n &= n - 1; // 消去最右边的1
        count++;
    }
    return count;
}

优势:时间复杂度 O (k),k 是 1 的个数,比逐位判断 O (n) 效率更高,是面试标准最优解。

2. 判断一个数是不是 2 的整数次幂

题目:判断一个正整数是不是 2 的幂。

思路:2 的幂的二进制有且仅有一个 1,用消去最低位 1 的技巧,消一次后变为 0。

复制代码
bool isPowerOfTwo(int n) {
    if (n <= 0) return false;
    return (n & (n - 1)) == 0;
}

3. 不用加减乘除做加法

题目:写一个函数,求两个整数之和,要求不能使用 +、-、*、/ 四则运算符号。

思路:用异或算无进位和,用与运算左移算进位,循环直到进位为 0。

复制代码
int add(int a, int b) {
    while (b != 0) {
        int carry = (unsigned int)(a & b) << 1; // 进位
        a = a ^ b; // 无进位和
        b = carry;
    }
    return a;
}

4. 找出数组中只出现一次的数字

题目:数组中只有一个数字出现一次,其他都出现两次,找出这个数字。

思路:利用异或特性,相同数字异或为 0,全部异或后剩下的就是只出现一次的数。

复制代码
int singleNumber(int* nums, int numsSize) {
    int res = 0;
    for (int i = 0; i < numsSize; i++) {
        res ^= nums[i];
    }
    return res;
}

五、面试高频考点与易错坑点

1. 经典面试问答

Q1:什么是大端小端?有哪些方法可以判断机器的字节序?

答: 大端是高字节存在低地址,低字节存在高地址;小端是低字节存在低地址,高字节存在高地址。 判断方法主要有两种:

  1. 联合体法:利用联合体共享内存的特性,给 int 赋值 1,读取 char 成员的值判断
  2. 指针法:将 int 指针强转为 char 指针,读取第一个字节判断 其中联合体法代码更简洁,是面试首选写法。

Q2:n & (n-1) 有什么作用?能解决哪些经典问题?

答: 作用是消去 n 二进制中最右边的一个 1。 可以解决的经典问题:

  1. 统计二进制中 1 的个数
  2. 判断一个数是不是 2 的整数次幂
  3. 判断一个数是不是 4 的幂的衍生题
  4. 消除二进制末尾连续的 0

Q3:左移和右移对于有符号数和无符号数有什么区别?

答:

  • 左移:两者都是低位补 0,高位丢弃。但有符号数左移如果改变了符号位,属于未定义行为。
  • 右移:无符号数是逻辑右移,高位补 0;有符号数是算术右移,高位补符号位,正数补 0,负数补 1。 工程中位操作推荐使用无符号类型,避免符号位带来的未定义行为。

Q4:位运算相比算术运算有什么优势?适用哪些场景?

答: 优势:位运算直接操作二进制,CPU 指令周期更短,执行效率更高,且不需要额外的硬件乘法器等资源。 适用场景:

  1. 嵌入式硬件寄存器操作
  2. 网络协议字段解析
  3. 标志位管理,节省内存
  4. 算法优化、加密校验算法
  5. 资源受限的单片机开发

Q5:异或运算有哪些核心特性?有哪些典型应用?

答: 核心特性:自身异或为 0,和 0 异或为自身,满足交换律结合律。 典型应用:

  1. 不用临时变量交换两个数
  2. 找出数组中只出现一次的数字
  3. 简单加密解密
  4. 数据校验
  5. 翻转指定比特位

2. 常见易错坑点

  1. 位操作使用有符号数,右移、左移出现符号位问题,引发未定义行为
  2. 移位运算符优先级低于加减,忘记加括号导致运算结果错误
  3. 移位位数超过数据类型的位数,属于 C 语言未定义行为,结果不可预期
  4. 寄存器位操作时,清位忘记取反,直接和 0 相与导致所有位都被清零
  5. 大小端处理时,混淆高低字节顺序,跨平台通信数据解析错误
  6. 位掩码使用有符号 1 左移,高位溢出后变成负数,引发后续逻辑错误
  7. 误以为位运算万能,过度使用导致代码可读性极差,维护成本飙升

以上就是 C 语言位运算的全部实战核心内容,从工程封装到笔试算法全覆盖,是嵌入式、底层开发必须熟练掌握的基础技能。


制作不易,如果对你有用,希望能点赞收藏支持一下。