当前位置:首页 > 技术学院 > 技术前线
[导读]在C语言开发中,位操作符是最容易被新手忽略,却能在嵌入式开发、底层驱动、算法优化中发挥巨大作用的工具。和常规的算术操作、逻辑操作相比,位操作直接操作二进制位,执行效率更高,占用代码空间更小,能轻松实现很多用常规方法很难实现的功能:比如状态标志位管理、寄存器配置、数据压缩、奇偶校验等等。

C语言开发中,位操作符是最容易被新手忽略,却能在嵌入式开发、底层驱动、算法优化中发挥巨大作用的工具。和常规的算术操作、逻辑操作相比,位操作直接操作二进制位,执行效率更高,占用代码空间更小,能轻松实现很多用常规方法很难实现的功能:比如状态标志位管理、寄存器配置、数据压缩、奇偶校验等等。

很多开发者对位操作的认知只停留在“与或非异或”的基础定义,不知道实际开发中有哪些实用的技巧。本文整理了嵌入式开发、系统开发中最常用的位操作小技巧,每个技巧都配了可直接复用的代码,帮你快速掌握位操作的精髓,写出更简洁高效的C语言代码。

一、先回顾基础:五个常用位操作符的核心特性

C语言一共提供了六种位操作符,最常用的是五个,我们先梳理它们的核心特性,这是所有技巧的基础:

操作符名称核心规则关键特性

&按位与对应位都为1结果才是1,否则为0和1相与保留原位,和0相与清零原位

``按位或对应位只要有一个为1结果就是1,否则为0

^按位异或对应位不同结果为1,相同为0和1异或翻转原位,和0异或保留原位,同一个数异或两次结果是原数

~按位取反所有0变1,1变0注意是所有位取反,包括符号位,整数类型结果是负数补码

<>左移/右移所有位向左/右移N位,左移补0,右移无符号数补0,有符号数补符号位左移N位等价于乘以2^N,右移N位等价于除以2^N(正整数)

这些基础规则看起来简单,但组合起来就能实现很多神奇的效果,下面我们来看实际开发中最常用的技巧。

二、状态标志位管理:用一个字节存8个标志

这是嵌入式开发中最常用的技巧,很多时候我们需要保存多个开关状态(比如外设的开/关、功能的使能/禁用),如果每个状态用一个bool变量保存,8个状态就要占用8字节,如果用位操作,只需要1字节就能存8个状态,节省8倍内存,对于RAM紧张的MCU来说非常实用。

1. 常用操作:置位、清零、取反、判断

针对一个32位整数中的某一位(我们要操作第n位,n从0开始计数),四个最基础的操作:

// 把第n位置1

#define SET_BIT(reg, n) ((reg) |= (1U << (n)))

// 把第n位清0

#define CLEAR_BIT(reg, n) ((reg) &= ~(1U << (n)))

// 把第n位翻转(0变1,1变0)

#define FLIP_BIT(reg, n) ((reg)^= (1U << (n)))

// 判断第n位是否为1

#define IS_BIT_SET(reg, n) (((reg) >> (n)) & 1U)

这里有一个细节:我们用的是1U(无符号1)而不是普通的1,避免移位的时候触发符号位扩展,导致出现负数错误,尤其是操作最高位(比如32位整数的第31位)的时候,用1U是更安全的做法。

2. 实际使用示例

比如我们要保存8个传感器的开关状态,只需要定义一个uint8_t类型的变量:

uint8_t sensor_status = 0; // 初始全是0,全部关闭

// 打开第3号传感器

SET_BIT(sensor_status, 3);

// 关闭第0号传感器

CLEAR_BIT(sensor_status, 0);

// 翻转第5号传感器状态

FLIP_BIT(sensor_status, 5);

// 判断第2号传感器是否打开

if (IS_BIT_SET(sensor_status, 2)) {

// 执行对应逻辑

}

一个字节存8个状态,要是需要10个状态,用两个字节(uint16_t)就能存,非常节省RAM,代码也很清晰,比用多个变量简洁太多。

三、提取指定位域:从一个整数中取出连续几位

有时候我们需要从一个寄存器或者字节中提取连续几个二进制位,比如串口协议的状态字节,高4位是命令类型,低4位是参数,怎么快速提取出高4位?

用位操作可以非常轻松实现:提取从第start位开始,共length位,方法是:先右移start位,把要提取的位移到最低位,然后和一个低length位全是1的掩码按位与,就能得到结果。

// 从data中提取从start位开始,长度为len的位段

#define EXTRACT_BITS(data, start, len) (((data) >> (start)) & ((1U << (len)) - 1))

这个技巧非常实用,我们测试几个例子:

// 例子1:提取0xAA(二进制10101010)的高4位,start=4,len=4

uint8_t data = 0xAA;

uint8_t high4 = EXTRACT_BITS(data, 4, 4);

// 结果是 0x0A = 10,和预期一致:1010就是十进制10

// 例子2:提取0x1234(二进制0001 0010 0011 0100)的第5位到第10位(共6位),start=5,len=6

uint16_t data = 0x1234;

uint16_t result = EXTRACT_BITS(data, 5, 6);

// 计算:0x1234 >>5 = 0x91(二进制10010001),(1<<6)-1=0x3F,0x91 & 0x3F = 0x11 = 17,结果正确

这个掩码生成的技巧(1 << len) -1非常巧妙,len位全是1的掩码,不需要自己手动算,代码自动生成,非常简洁。如果是修改连续几个位,也可以用类似的思路:先把原来的对应位清零,然后把新值左移start位,再或进去:

// 把data中从start开始长度为len的位段设置为value

#define SET_BITS(data, start, len, value) \

do { \

(data) &= ~(((1U << (len)) - 1) << (start)); \

(data) |= ((value) & ((1U << (len)) - 1)) << (start); \

} while(0)

这种写法在寄存器配置的时候非常常用,比如修改定时器的预分频器位域,只需要一句宏就能搞定,不需要手动计算掩码。

四、用异或实现两个变量交换,不需要临时变量

这是位操作非常经典的一个技巧,交换两个整数的值,不需要额外定义临时变量,原理就是利用异或的三个特性:a^ a = 0、a^ 0 = a、异或满足交换律结合律。

交换代码如下:

void swap(int *a, int *b) {

if (a != b) { // 必须判断地址不同,否则自己和自己异或会清零

*a^= *b;

*b^= *a;

*a^= *b;

}

}

原理拆解一下:

第一步*a^= *b:现在*a保存了a^ b,*b还是原来的b;

第二步*b^= *a:*b = b^ (a^ b) = a^ (b^ b) = a^ 0 = a,现在*b已经变成原来的a;

第三步*a^= *b:*a = (a^ b)^ a = b^ (a^ a) = b^0 = b,现在*a变成原来的b; 交换完成。

不过这个技巧现在更多是用于面试题,实际开发中不推荐用:一方面现在编译器对临时变量交换已经优化的很好,效率比异或交换差不多,甚至更高;另一方面代码可读性差,还必须处理地址相同的特殊情况,容易出bug。但是作为位操作的经典技巧,我们还是需要知道原理。

五、判断整数是不是2的幂

这也是面试和实际开发都常用的技巧,判断一个正整数是不是2的幂,2的幂有一个非常明显的二进制特性:二进制中只有一个1,剩下全是0,比如:

1 = 0b1,只有1个1

2 = 0b10,只有1个1

4 = 0b100,只有1个1

8 = 0b1000,只有1个1

利用这个特性,加上位操作可以快速判断,公式是(n & (n - 1)) == 0,原理是什么呢?我们看例子: n=8(0b1000),n-1就是0b0111,按位与之后:

1000

& 0111

= 0000

结果就是0,如果不是2的幂,比如n=6(0b110),n-1=0b101,按位与是0b100≠0,结果不为0。

所以判断代码可以写成:

bool is_power_of_two(unsigned int n) {

return (n & (n - 1)) == 0;

}

这个写法比循环计数二进制中1的个数快很多,只需要一次位操作就能出结果,效率极高。这里需要注意n=0的时候,结果也是0,会被误判,所以如果包含0的场景,要加上n !=0的判断:return n && ((n & (n -1)) ==0);。

延伸技巧:如果要统计一个整数二进制中1的个数,可以用这个方法循环:每次n &= n-1,都会消掉最后一个1,循环次数就是1的个数,比逐位判断快很多:

int count_one_bits(unsigned int n) {

int count = 0;

while(n) {

n &= n - 1;

count++;

}

return count;

}

六、快速判断奇偶,不用取模

判断一个整数是奇数还是偶数,其实只需要看最低位:最低位是1就是奇数,0就是偶数,直接用位操作就能判断,比取模%2更快:

bool is_odd(unsigned int n) {

return (n & 1U) == 1;

}

bool is_even(unsigned int n) {

return (n & 1U) == 0;

}

对于现代编译器来说,其实n%2也会被优化成位操作,但是直接写位操作更直观,也能体现对二进制的理解。

七、高低字节交换,快速大小端转换

很多时候我们需要做大小端转换,比如把16位整数的高低字节交换,把32位整数的字节序反转,用位操作可以非常高效实现:

1. 16位高低字节交换

uint16_t swap16(uint16_t val) {

return (val >> 8) | (val << 8);

}

原理就是把高8位右移到低8位,低8位左移到高8位,然后按位或起来,一步完成,不需要额外变量,非常简洁。

2. 32位整数字节序反转

uint32_t swap32(uint32_t val) {

return ((val & 0x000000FF) << 24) |

((val & 0x0000FF00) << 8) |

((val & 0x00FF0000) >> 8) |

((val & 0xFF000000) >> 24);

}

每个字节提取出来,放到对应的位置,一次计算就能完成转换,比循环处理每个字节效率高很多。实际开发中处理网络字节序、传感器数据的时候经常用到,非常方便。

八、把整数向上对齐到2的幂的整数倍

在内存分配、帧缓存设计中,我们经常需要把一个长度向上对齐到N(N是2的幂)的整数倍,比如对齐到4字节、8字节、16字节,用位操作可以快速实现:

// 把size向上对齐到align的整数倍,align必须是2的幂

unsigned int align_up(unsigned int size, unsigned int align) {

return (size + align - 1) & ~(align - 1);

}

我们测试几个例子:

对齐到4字节:size=5,align=4,(5+4-1) & ~(4-1) = 8 & ~3 = 8 & 0x...11111100 = 8,正确,5向上对齐到4就是8;

对齐到16字节:size=20,align=16,(20+16-1) & ~15 = 35 & 0x...11110000 = 32,正确,20对齐到16就是32;

size刚好是align的倍数:size=8,align=4,(8+4-1) & ~3 = 11 & ~3 = 8,结果还是8,正确。

这个技巧非常常用,很多内存管理器、链表节点对齐都用这个写法,一句代码搞定对齐,非常高效。如果是向下对齐,更简单:return size & ~(align -1);就可以。

九、快速计算最低位的1的位置

很多算法需要找到一个整数中最低位的1是第几位,比如0b10100最低位的1在第2位(从0开始),用位操作可以快速计算,利用n & -n的特性:这个操作会保留最低位的1,其他位都清零,原理是负数用补码存储,-n = ~n +1,按位与之后刚好保留最低位的1。

比如n=0b10100,-n补码是...11101100,按位与之后结果就是0b100,也就是4,然后我们算一下这个结果是2的几次方,就是对应的位置。

代码实现:

// 找最低位1的位置,返回从0开始的位号,n不能为0

int find_lowest_bit(unsigned int n) {

unsigned int lowest_one = n & -n; // 提取最低位的1

int pos = 0;

while(lowest_one >>= 1) {

pos++;

}

// 如果是x86架构,还可以用内置函数更快:pos = __builtin_ctz(n);

return pos;

}

GCC编译器还提供了内置函数__builtin_ctz(count trailing zeros),直接返回末尾0的个数,也就是最低位1的位置,一条指令就能完成,比循环快很多,适合性能要求高的场景。

十、寄存器配置中的实用技巧:清零后再赋值

STM32等MCU开发中,修改寄存器的某个位域的时候,一定要先清零原来的位,再设置新值,不能直接或进去,否则原来的残留位会影响结果,正确写法是:

// 错误写法:直接或,原来的位没有清零,会残留错误值

// USART1->BRR |= (9600 << 4);

// 正确写法:先清零对应位域,再或新值

USART1->BRR &= ~(USART_BRR_DIV_FRACTION); // 先清零分数位域

USART1->BRR |= (my_div & USART_BRR_DIV_FRACTION); // 再设置新值

我们之前的SET_BITS宏也是这个逻辑,先清零再赋值,这是寄存器配置的标准写法,很多新手容易忘记清零,导致寄存器配置错误,出现莫名其妙的bug。

十一、使用位操作的注意事项

虽然位操作很好用,但也有几个坑需要注意,避免出错:

移位的位数不要超过类型的位数:比如32位int类型,不要移32位以上,行为是未定义的,不同编译器结果不一样,一定要注意,操作n位类型,移位位数最大是n-1。

注意符号位的问题:有符号整数右移会补符号位,导致结果不对,所以位操作尽量用无符号类型(uint8_t/uint16_t/uint32_t),或者用1U而不是1,避免符号位扩展。

宏定义要加足够的括号:位操作的宏里面每个参数都要加括号,否则运算符优先级会导致结果错误,比如1 << n + 1会先算n+1再移位,和预期不一样,所以宏里面每个变量都要加括号。

不要过度追求位操作:现在编译器优化能力很强,可读性优先,性能差不多的情况下,优先用可读性好的写法,不要为了炫技用位操作,导致代码难维护。

总结

位操作是C语言开发者的基本功,这些小技巧看起来简单,但在实际开发中非常实用:既能节省内存空间,又能提升执行效率,很多场景下用一句位操作就能搞定的事情,用常规方法要写好几行循环,代码简洁很多。

上面整理的这些技巧,都是开发中经常用到的,状态位管理、提取位域、对齐、大小端转换这些,几乎每个嵌入式项目都会用到,记住这些写法,需要的时候直接拿来用,能帮你节省很多时间,写出更高效简洁的C语言代码。如果能熟练掌握这些技巧,也能体现你对C语言和二进制的理解深度,不管是开发还是面试,都能得心应手。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
换一批
延伸阅读

当我们谈起C语言,很多人第一印象是面向底层、面向系统的编译型语言,写出来的程序一般都是从头到尾跑一遍就结束,很少和用户交互。但实际上,C语言从诞生开始就支持交互式的程序设计,通过标准输入输出和用户实时交互,接收用户输入、...

关键字: C语言 编程

在C语言开发中,原生字符串的使用一直存在诸多不便。传统C语言中,字符串本质是以'\0'结尾的固定字符数组,开发人员必须提前预估字符串的最大长度:如果预估过小,拼接或插入字符时会出现缓冲区溢出,引发内存越界错误;如果预估过...

关键字: C语言 字符串

在高并发、低延迟的现代软件系统中,内存管理的效率直接决定了系统的整体性能。传统的动态内存分配方式(如C++中的new/delete、C语言中的malloc/free)虽然使用便捷,但在频繁分配和释放内存的场景下,会产生严...

关键字: C语言 内存

在无人机、机器人等智能设备中,九轴IMU(惯性测量单元)是姿态解算的核心传感器,但其原始数据受噪声和零偏影响严重。卡尔曼滤波作为一种基于概率的最优估计方法,通过融合加速度计、陀螺仪和磁力计数据,可显著提升姿态解算的精度与...

关键字: 卡尔曼滤波 九轴IMU C语言

在嵌入式开发中,C语言编写的代码最终会被编译器转化为机器指令,而理解这一转化过程对优化程序性能至关重要。通过反编译工具观察不同优化等级下的汇编代码,开发者能直观看到编译器的"思考方式",从而写出更高效的C代码。

关键字: C语言 反编译工具 编译器

在嵌入式系统开发中,C语言凭借其高效性和接近硬件的特性成为首选语言。然而,这种"贴近硬件"的特性也暗藏危机——内存对齐问题和指针类型转换错误就像隐藏在代码中的定时炸弹,轻则导致性能下降,重则引发硬件异常。本文通过实际案例...

关键字: C语言 嵌入式开发

在单片机开发领域,C语言凭借其高效、易维护和可移植性强的特性,成为了开发者的首选编程语言。而延时程序作为单片机程序中控制时序、协调各模块运行的关键组成部分,其编写的合理性直接影响到整个系统的稳定性与可靠性。然而,看似简单...

关键字: 单片机 C语言

在电子技术飞速发展的当下,单片机作为嵌入式系统的核心部件,广泛应用于工业控制、智能家居、汽车电子等众多领域。对于开发者而言,掌握单片机开发的基本技巧,不仅能提升开发效率,还能优化产品性能、降低成本。

关键字: 单片机 C语言

在电子技术飞速发展的当下,单片机作为嵌入式系统的核心部件,广泛应用于工业控制、智能家居、汽车电子等众多领域。对于开发者而言,掌握单片机开发的基本技巧,不仅能提升开发效率,还能优化产品性能、降低成本。

关键字: 单片机 C语言

Linux内核模块开发是操作系统底层编程的核心技能,字符设备驱动作为最常见的驱动类型,其开发流程涵盖设备号管理、内核对象注册、文件操作映射等关键环节。本文以C语言实现为例,系统阐述字符设备驱动的开发流程、核心原理及调试技...

关键字: Linux内核 C语言
关闭