C语言位运算

引言:为什么需要掌握位运算?

在计算机底层,所有的数据都以二进制形式存储和处理。位运算(Bitwise Operations)作为直接操作二进制位的工具,是高性能计算、嵌入式开发、算法优化的核心技能。今天我们将通过一个实际案例,深入理解**与(&)、或(|)、异或(^)、取反(~)、移位(<<、>>)**等位运算的应用场景,并实现一组实用的位操作函数。


位运算基础:二进制视角下的数字

要掌握位运算,首先需要理解二进制数的表示规则:

  • 最低有效位(LSB):二进制数的最右边一位(2⁰位),决定数的奇偶性;
  • 高位:从右往左依次为2¹、2²...2ⁿ位,每一位代表2的幂次;
  • 补码表示:负数在内存中以补码形式存储(原码取反+1),这是位运算处理负数的关键。

实战函数解析:逐个击破位运算问题

1. 判断奇数:最低位的「1」密码

问题描述:判断一个整数是否为奇数。
​位运算思路​​:奇数的二进制最低位一定是1,偶数的最低位是0。因此,只需将数字与1(二进制000...0001)进行按位与(&)操作,若结果为1则是奇数,否则是偶数。

代码实现

1
2
3
4
5
void is_odd(int num) {
// 奇数的二进制最低位一定是1,用 num & 1 保留最低位
int result = num & 1;
printf("数字 %d 是否为奇数?%s\n", num, result ? "是" : "否");
}

示例验证

  • 输入5(二进制101):5 & 1 = 1 → 奇数;
  • 输入6(二进制110):6 & 1 = 0 → 偶数。

2. 判断2的幂:二进制中的「孤独1」

问题描述:判断一个正整数是否是2的幂(如1=2⁰,2=2¹,4=2²...)。
​位运算思路​​:2的幂的二进制表示中只有一个1(如8=1000₂),若将其减1(如8-1=7=0111₂),则原数与减1后的数按位与结果必为0(1000 & 0111 = 0000)。注意:0和负数不可能是2的幂。

代码实现

1
2
3
4
5
6
7
8
9
10
void is_Power_of_two(int num) {
// 2的幂必须是正整数
if (num <= 0) {
printf("数字 %d 不是2的幂(需为正整数)\n", num);
return;
}
// 若 num 是2的幂,则 num & (num - 1) 必为0(如8=1000,8-1=0111,与运算结果为0)
int result = (num & (num - 1)) == 0;
printf("数字 %d 是否为2的幂?%s\n", num, result ? "是" : "否");
}

示例验证

  • 输入8(1000₂):8 & 7 = 0 → 是2的幂;
  • 输入6(0110₂):6 & 5 = 4 ≠ 0 → 不是。

3. 找最低有效位(LSB):定位第一个「1」

问题描述:给定非零整数,找出其值为1的最低有效位的位置(如6=110₂,最低有效位是第2位,对应值为2¹=2)。
​位运算思路​​:利用补码特性,负数的补码是原码取反+1。因此,num & (-num)会将原数中最低位的1保留,其余位清零(如6=000...0110,-6=111...1010,6 & -6=000...0010=2)。

代码实现

1
2
3
4
5
6
7
8
9
void find_lsb(int num) {
if (num == 0) { // 0没有有效位
printf("数字0没有最低有效位\n");
return;
}
// 利用补码特性:num & (-num) 会保留最低位的1,其余位清零
int lsb_value = num & (-num);
printf("数字 %d 的最低有效位值是:%d\n", num, lsb_value);
}

示例验证

  • 输入6(0110₂):6 & -6 = 2(0010₂)→ 最低有效位是2;
  • 输入12(1100₂):12 & -12 = 4(0100₂)→ 最低有效位是4。

4. 交换数值:异或的自反魔法

问题描述:不使用临时变量,交换两个整数的值。
​位运算思路​​:异或(^)满足以下性质:

  • a ^ a = 0(相同数异或为0);
  • a ^ 0 = a(任何数异或0不变);
  • 异或满足交换律和结合律。

利用这些性质,可通过三次异或操作完成交换:

1
2
3
4
5
6
void change(int *a, int *b) {
if (a == b) return; // 避免相同地址异或导致结果为0
*a ^= *b; // a = a ^ b
*b ^= *a; // b = (a ^ b) ^ b = a
*a ^= *b; // a = (a ^ b) ^ a = b
}

修正说明:原代码中change函数参数为值传递(int a, int b),无法修改主函数中的变量。正确实现需使用指针(int *a, int *b)。


5. 寻找唯一元素:异或的「分组」艺术

问题描述:给定一个非空整数数组,除某个元素只出现一次外,其余元素均出现两次。找出这个唯一元素(扩展:若有两个元素各出现一次,其余出现两次,如何找出这两个元素?)。

场景1:仅一个唯一元素

位运算思路:利用异或性质,相同数异或结果为0,0异或任何数为自身。因此,遍历数组异或所有元素,最终结果即为唯一元素。

代码实现

1
2
3
4
5
6
7
int find_only(int nums[], int length) {
int result = 0;
for (int i = 0; i < length; i++) {
result ^= nums[i]; // 异或性质:相同数异或为0,0异或任何数为自身
}
return result;
}

场景2:两个唯一元素(扩展)

位运算思路

  1. 先异或所有元素,得到两个唯一元素的异或结果(记为lsb);
  2. lsb的二进制中至少有一位是1(因为两数不同),找到最低位的1(即lsb & (-lsb));
  3. 根据该位将数组分为两组(该位为1和该位为0),每组内的元素异或结果即为两个唯一元素。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void find_two(int nums[], int length) {
if (length < 2) {
printf("数组长度至少为2\n");
return;
}

// 第一轮异或:得到两个唯一元素的异或结果(记为 xor_sum)
int xor_sum = 0;
for (int i = 0; i < length; i++) {
xor_sum ^= nums[i];
}

// 找到 xor_sum 中最低位的1(该位是两个唯一元素不同的位)
int lsb = xor_sum & (-xor_sum);

// 第二轮异或:按 lsb 分组异或,每组结果即为一个唯一元素
int a = 0, b = 0;
for (int i = 0; i < length; i++) {
if (nums[i] & lsb) { // 该位为1的元素分到组a
a ^= nums[i];
}
else { // 该位为0的元素分到组b
b ^= nums[i];
}
}
printf("数组中两个唯一出现的元素是:%d 和 %d\n", a, b);
}

示例验证

  • 数组[2, 3, 2, 4]:唯一元素是3和4;
    第一轮异或:2^3^2^4 = 3^4 = 7(二进制111);
    lsb = 7 & (-7) = 1(二进制001);
    分组异或:3(011)在组1(lsb=1),4(100)在组2(lsb=0),结果a=3,b=4。

注意事项与常见陷阱

  1. 指针传递的重要性change函数若使用值传递无法修改原变量,必须用指针(int *);
  2. 负数处理:位运算中负数以补码形式存在,需注意符号位的影响(如-1的二进制是全1);
  3. 移位溢出:左移操作(<<)可能导致高位丢失(如int类型左移32位结果未定义);
  4. 边界条件:判断2的幂时需排除0和负数(如num=0num&(num-1)会引发错误)。

总结:位运算的核心价值

位运算不仅是C语言的基础技能,更是理解计算机底层原理的关键。通过本文的实战案例,我们掌握了:

  • 如何用位运算快速判断奇偶、2的幂;
  • 如何定位最低有效位;
  • 如何用异或实现无临时变量交换;
  • 如何用异或分组解决复杂唯一元素问题。

下次遇到类似问题时,不妨尝试用二进制视角重新审视数据——位运算的简洁与高效,会让你惊叹于计算机世界的「位」妙!

提示:实际开发中需注意位运算的可读性,避免过度优化导致代码难以维护。对于复杂场景(如大端小端处理),建议结合具体硬件架构文档进行适配。


附录:完整源代码与文件说明

为了方便读者复现与调试,以下是项目的完整源代码,并附各文件功能说明。


1. function.h —— 头文件(函数声明与宏定义)

文件作用
声明需要实现的位运算函数,以及必要的宏定义(如防止头文件重复包含)。头文件是C语言中实现模块化编程的关键,通过#include指令被其他源文件引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef FUNCTION_H  // 防止头文件重复包含
#define FUNCTION_H

// 函数声明:判断整数是否为奇数
void is_odd(int num);

// 函数声明:判断是否为2的幂
void is_Power_of_two(int num);

// 函数声明:查找最低有效位(Last Set Bit)
void find_lsb(int num);

// 函数声明:交换两个整数的值(异或实现)
void change(int *a, int *b); // 注意:必须用指针传递才能修改原变量

// 函数声明:查找数组中唯一出现一次的元素(其余出现两次)
int find_only(int nums[], int length);

// 函数声明:查找数组中两个唯一出现一次的元素(其余出现两次)
void find_two(int nums[], int length);

#endif

2. function.c —— 源文件(函数实现)

文件作用
实现function.h中声明的所有位运算函数。将函数实现与声明分离,符合C语言的模块化编程规范,便于维护与代码复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#include <stdio.h>
#include "function.h"

// 函数实现:判断整数是否为奇数(最低位是否为1)
void is_odd(int num) {
// 奇数的二进制最低位一定是1,用 num & 1 保留最低位
int result = num & 1;
printf("数字 %d 是否为奇数?%s", num, result ? "是" : "否");
}

// 函数实现:判断是否为2的幂(二进制仅有一个1)
void is_Power_of_two(int num) {
// 2的幂必须是正整数
if (num <= 0) {
printf("数字 %d 不是2的幂(需为正整数)", num);
return;
}
// 若 num 是2的幂,则 num & (num - 1) 必为0(如8=1000,8-1=0111,与运算结果为0)
int result = (num & (num - 1)) == 0;
printf("数字 %d 是否为2的幂?%s", num, result ? "是" : "否");
}

// 函数实现:查找最低有效位(Last Set Bit)
void find_lsb(int num) {
if (num == 0) { // 0没有有效位
printf("数字0没有最低有效位");
return;
}
// 利用补码特性:num & (-num) 会保留最低位的1,其余位清零
int lsb_value = num & (-num);
printf("数字 %d 的最低有效位值是:%d(对应2^%d位)", num, lsb_value, __builtin_ctz(lsb_value));
// __builtin_ctz计算末尾0的个数(GCC内置函数)
}

// 函数实现:交换两个整数的值(异或无临时变量版)
void change(int *a, int *b) {
if (a == b) return; // 避免相同地址异或导致结果为0
*a ^= *b; // a = a ^ b
*b ^= *a; // b = (a ^ b) ^ b = a
*a ^= *b; // a = (a ^ b) ^ a = b
}

// 函数实现:查找数组中唯一出现一次的元素(其余出现两次)
int find_only(int nums[], int length) {
int result = 0;
for (int i = 0; i < length; i++) {
result ^= nums[i]; // 异或性质:相同数异或为0,0异或任何数为自身
}
return result;
}

// 函数实现:查找数组中两个唯一出现一次的元素(其余出现两次)
void find_two(int nums[], int length) {
if (length < 2) {
printf("数组长度至少为2");
return;
}

// 第一轮异或:得到两个唯一元素的异或结果(记为 xor_sum)
int xor_sum = 0;
for (int i = 0; i < length; i++) {
xor_sum ^= nums[i];
}

// 找到 xor_sum 中最低位的1(该位是两个唯一元素不同的位)
int lsb = xor_sum & (-xor_sum);

// 第二轮异或:按 lsb 分组异或,每组结果即为一个唯一元素
int a = 0, b = 0;
for (int i = 0; i < length; i++) {
if (nums[i] & lsb) { // 该位为1的元素分到组a
a ^= nums[i];
} else { // 该位为0的元素分到组b
b ^= nums[i];#include <stdio.h>
#include "function.h"

// 函数实现:判断整数是否为奇数(最低位是否为1)
void is_odd(int num) {
// 奇数的二进制最低位一定是1,用 num & 1 保留最低位
int result = num & 1;
printf("数字 %d 是否为奇数?%s\n", num, result ? "是" : "否");
}

// 函数实现:判断是否为2的幂(二进制仅有一个1)
void is_Power_of_two(int num) {
// 2的幂必须是正整数
if (num <= 0) {
printf("数字 %d 不是2的幂(需为正整数)\n", num);
return;
}
// 若 num 是2的幂,则 num & (num - 1) 必为0(如8=1000,8-1=0111,与运算结果为0)
int result = (num & (num - 1)) == 0;
printf("数字 %d 是否为2的幂?%s\n", num, result ? "是" : "否");
}

// 函数实现:查找最低有效位(Last Set Bit)
void find_lsb(int num) {
if (num == 0) { // 0没有有效位
printf("数字0没有最低有效位\n");
return;
}
// 利用补码特性:num & (-num) 会保留最低位的1,其余位清零
int lsb_value = num & (-num);
printf("数字 %d 的最低有效位值是:%d\n", num, lsb_value);
}

// 函数实现:交换两个整数的值(异或无临时变量版)
void change(int *a, int *b) {
if (a == b) return; // 避免相同地址异或导致结果为0
*a ^= *b; // a = a ^ b
*b ^= *a; // b = (a ^ b) ^ b = a
*a ^= *b; // a = (a ^ b) ^ a = b
}

// 函数实现:查找数组中唯一出现一次的元素(其余出现两次)
int find_only(int nums[], int length) {
int result = 0;
for (int i = 0; i < length; i++) {
result ^= nums[i]; // 异或性质:相同数异或为0,0异或任何数为自身
}
return result;
}

// 函数实现:查找数组中两个唯一出现一次的元素(其余出现两次)
void find_two(int nums[], int length) {
if (length < 2) {
printf("数组长度至少为2\n");
return;
}

// 第一轮异或:得到两个唯一元素的异或结果(记为 xor_sum)
int xor_sum = 0;
for (int i = 0; i < length; i++) {
xor_sum ^= nums[i];
}

// 找到 xor_sum 中最低位的1(该位是两个唯一元素不同的位)
int lsb = xor_sum & (-xor_sum);

// 第二轮异或:按 lsb 分组异或,每组结果即为一个唯一元素
int a = 0, b = 0;
for (int i = 0; i < length; i++) {
if (nums[i] & lsb) { // 该位为1的元素分到组a
a ^= nums[i];
}
else { // 该位为0的元素分到组b
b ^= nums[i];
}
}

printf("数组中两个唯一出现的元素是:%d 和 %d\n", a, b);
}

3. main.c —— 主程序入口

文件作用
程序的入口函数(main函数),负责调用function.c中实现的位运算函数,完成测试逻辑。用户通过main函数输入数据,触发各个位运算功能的演示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#define _CRT_SECURE_NO_WARNINGS  // 关闭VS编译器的安全警告(如scanf)
#include <stdio.h>
#include "function.h" // 包含函数声明

int main(void) {
int num = 0;
printf("请输入你要查询的整数:\n");
scanf(" %d", &num);
// 输入整数(注意空格跳过换行符)

// 测试单个数字的位运算功能
is_odd(num); // 判断奇偶
is_Power_of_two(num); // 判断是否为2的幂
find_lsb(num); // 查找最低有效位

// 测试数组的唯一元素查找功能(示例数组)
int test_array[] = { 2, 3, 2, 4, 5, 5, 3, 6 };
// 示例数组:2、4、6各出现一次,其余出现两次
int array_length = sizeof(test_array) / sizeof(test_array[0]);
// 计算数组长度

printf("--- 数组唯一元素测试 ---\n");
find_two(test_array, array_length);
// 查找两个唯一元素(本例中是4和6)

// 测试交换函数(可选)
int a = 10, b = 20;
printf("交换前:a=%d, b=%d\n", a, b);
change(&a, &b); // 传递指针以修改原变量
printf("交换后:a=%d, b=%d\n", a, b);
return 0;
}

通过以上代码,读者可直接运行并验证位运算的实战效果,加深对二进制操作的理解。