前言:
位运算是嵌入式底层、硬件驱动、网络协议、笔试算法的核心刚需能力,直接操作二进制比特位,执行效率远高于普通算术运算,是底层开发者的必备技能。本篇从基础运算符、工程常用技巧,到硬件寄存器操作、大小端处理与笔试高频手撕题全覆盖,全部为工业界真实落地用法,兼顾面试考点与工程实战价值。
一、六大基础位运算符核心用法
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:什么是大端小端?有哪些方法可以判断机器的字节序?
答: 大端是高字节存在低地址,低字节存在高地址;小端是低字节存在低地址,高字节存在高地址。 判断方法主要有两种:
- 联合体法:利用联合体共享内存的特性,给 int 赋值 1,读取 char 成员的值判断
- 指针法:将 int 指针强转为 char 指针,读取第一个字节判断 其中联合体法代码更简洁,是面试首选写法。
Q2:n & (n-1)有什么作用?能解决哪些经典问题?
答: 作用是消去 n 二进制中最右边的一个 1。 可以解决的经典问题:
- 统计二进制中 1 的个数
- 判断一个数是不是 2 的整数次幂
- 判断一个数是不是 4 的幂的衍生题
- 消除二进制末尾连续的 0
Q3:左移和右移对于有符号数和无符号数有什么区别?
答:
- 左移:两者都是低位补 0,高位丢弃。但有符号数左移如果改变了符号位,属于未定义行为。
- 右移:无符号数是逻辑右移,高位补 0;有符号数是算术右移,高位补符号位,正数补 0,负数补 1。 工程中位操作推荐使用无符号类型,避免符号位带来的未定义行为。
Q4:位运算相比算术运算有什么优势?适用哪些场景?
答: 优势:位运算直接操作二进制,CPU 指令周期更短,执行效率更高,且不需要额外的硬件乘法器等资源。 适用场景:
- 嵌入式硬件寄存器操作
- 网络协议字段解析
- 标志位管理,节省内存
- 算法优化、加密校验算法
- 资源受限的单片机开发
Q5:异或运算有哪些核心特性?有哪些典型应用?
答: 核心特性:自身异或为 0,和 0 异或为自身,满足交换律结合律。 典型应用:
- 不用临时变量交换两个数
- 找出数组中只出现一次的数字
- 简单加密解密
- 数据校验
- 翻转指定比特位
2. 常见易错坑点
- 位操作使用有符号数,右移、左移出现符号位问题,引发未定义行为
- 移位运算符优先级低于加减,忘记加括号导致运算结果错误
- 移位位数超过数据类型的位数,属于 C 语言未定义行为,结果不可预期
- 寄存器位操作时,清位忘记取反,直接和 0 相与导致所有位都被清零
- 大小端处理时,混淆高低字节顺序,跨平台通信数据解析错误
- 位掩码使用有符号 1 左移,高位溢出后变成负数,引发后续逻辑错误
- 误以为位运算万能,过度使用导致代码可读性极差,维护成本飙升
以上就是 C 语言位运算的全部实战核心内容,从工程封装到笔试算法全覆盖,是嵌入式、底层开发必须熟练掌握的基础技能。
制作不易,如果对你有用,希望能点赞收藏支持一下。