C语言之 位运算
12 位运算
前面介绍的各种运算都是以字节作为最基本位进行的。 但在很多系统程序中常要求在位(bit)一级进行运算或处理。C语言提供了位运算的功能,这使得C语言也能像汇编语言一样用来编写系统程序。
12.1 位运算符C语言提供了六种位运算符:
& 按位与
| 按位或
^ 按位异或
~ 取反
<< 左移
>> 右移
12.1.1 按位与运算
按位与运算符"&"是双目运算符。其功能是参与运算的两数各对应的二进位相与。只有对应的两个二进位均为1时,结果位才为1,否则为0。参与运算的数以补码方式出现。
例如:9&5可写算式如下:
00001001 (9的二进制补码)
&00000101 (5的二进制补码)
00000001 (1的二进制补码)
可见9&5=1。
按位与运算通常用来对某些位清0或保留某些位。例如把a 的高八位清 0 ,保留低八位,可作a&255运算( 255 的二进制数为0000000011111111)。
【例12.1】
main(){
int a=9,b=5,c;
c=a&b;
printf("a=%d\nb=%d\nc=%d\n",a,b,c);
}
12.1.2 按位或运算
按位或运算符“|”是双目运算符。其功能是参与运算的两数各对应的二进位相或。只要对应的二个二进位有一个为1时,结果位就为1。参与运算的两个数均以补码出现。
例如:9|5可写算式如下:
00001001
|00000101
00001101 (十进制为13)可见9|5=13
【例12.2】
main(){
int a=9,b=5,c;
c=a|b;
printf("a=%d\nb=%d\nc=%d\n",a,b,c);
}
12.1.3 按位异或运算
按位异或运算符“”是双目运算符。其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。参与运算数仍以补码出现,例如95可写成算式如下:
00001001
^00000101
00001100 (十进制为12)
【例12.3】
main(){
int a=9;
a=a^5;
printf("a=%d\n",a);
}
12.1.4 求反运算
求反运算符~为单目运算符,具有右结合性。其功能是对参与运算的数的各二进位按位求反。
例如~9的运算为:
~(0000000000001001)结果为:1111111111110110
12.1.5 左移运算
左移运算符“<<”是双目运算符。其功能把“<< ”左边的运算数的各二进位全部左移若干位,由“<<”右边的数指定移动的位数,高位丢弃,低位补0。
例如:
a<<4
指把a的各二进位向左移动4位。如a=00000011(十进制3),左移4位后为00110000(十进制48)。
12.1.6 右移运算
右移运算符“>>”是双目运算符。其功能是把“>> ”左边的运算数的各二进位全部右移若干位,“>>”右边的数指定移动的位数。
例如:
设 a=15,
a>>2
表示把000001111右移为00000011(十进制3)。
应该说明的是,对于有符号数,在右移时,符号位将随同移动。当为正数时,最高位补0,而为负数时,符号位为1,最高位是补0或是补1 取决于编译系统的规定。Turbo C和很多系统规定为补1。
【例12.4】
main(){
unsigned a,b;
printf("input a number: ");
scanf("%d",&a);
b=a>>5;
b=b&15;
printf("a=%d\tb=%d\n",a,b);
}
请再看一例!
【例12.5】
main(){
char a='a',b='b';
int p,c,d;
p=a;
p=(p<<8)|b;
d=p&0xff;
c=(p&0xff00)>>8;
printf("a=%d\nb=%d\nc=%d\nd=%d\n",a,b,c,d);
}
12.2 位域(位段)
有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1两种状态,用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。
所谓“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
- 位域的定义和位域变量的说明
位域定义与结构定义相仿,其形式为:
struct 位域结构名
{ 位域列表 };
其中位域列表的形式为:
类型说明符 位域名:位域长度
例如:
struct bs
{
int a:8;
int b:2;
int c:6;
};
位域变量的说明与结构变量说明的方式相同。 可采用先定义后说明,同时定义说明或者直接说明这三种方式。
例如:
struct bs
{
int a:8;
int b:2;
int c:6;
}data;
说明data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。
对于位域的定义尚有以下几点说明:
- 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。
例如:
struct bs
{
unsigned a:4
unsigned :0 /*空域*/
unsigned b:4 /*从下一单元开始存放*/
unsigned c:4
}
在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。
- 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。
- 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:
struct k
{
int a:1
int :2 /*该2位不能使用*/
int b:3
int c:2
};
从以上分析可以看出,位域在本质上就是一种结构类型,不过其成员是按二进位分配的。
2. 位域的使用
位域的使用和结构成员的使用相同,其一般形式为:
位域变量名•位域名
位域允许用各种格式输出。
【例12.6】
main(){
struct bs
{
unsigned a:1;
unsigned b:3;
unsigned c:4;
} bit,*pbit;
bit.a=1;
bit.b=7;
bit.c=15;
printf("%d,%d,%d\n",bit.a,bit.b,bit.c);
pbit=&bit;
pbit->a=0;
pbit->b&=3;
pbit->c|=1;
printf("%d,%d,%d\n",pbit->a,pbit->b,pbit->c);
}
上例程序中定义了位域结构bs,三个位域为a,b,c。说明了bs类型的变量bit和指向bs类型的指针变量pbit。这表示位域也是可以使用指针的。程序的9、10、11三行分别给三个位域赋值(应注意赋值不能超过该位域的允许范围)。程序第12行以整型量格式输出三个域的内容。第13行把位域变量bit的地址送给指针变量pbit。第14行用指针方式给位域a重新赋值,赋为0。第15行使用了复合的位运算符"&=“,该行相当于:
pbit->b=pbit->b&3
位域b中原有值为7,与3作按位与运算的结果为3(111&011=011,十进制值为3)。同样,程序第16行中使用了复合位运算符”|=",相当于:
pbit->c=pbit->c|1
其结果为15。程序第17行用指针方式输出了这三个域的值。
12.3 位运算的实际应用
设置位
要设置某一位为1,可以使用按位或运算符|和一个合适的位掩码。例如,要将整数a的第3位置1:
int a = 5; // 二进制: 0101
int mask = 1 << 2; // 二进制: 0100
a = a | mask; // 结果: 0111 (即7)
清除位
要清除某一位为0,可以使用按位与运算符&和一个取反的位掩码。例如,要将整数a的第3位清0:
int a = 7; // 二进制: 0111
int mask = ~(1 << 2); // 二进制: 1011
a = a & mask; // 结果: 0011 (即3)
切换位
要切换某一位(0变1,1变0),可以使用按位异或运算符^和一个合适的位掩码。例如,要切换整数a的第3位:
int a = 5; // 二进制: 0101
int mask = 1 << 2; // 二进制: 0100
a = a ^ mask; // 结果: 0001 (即1)
检查位
要检查某一位是0还是1,可以使用按位与运算符&和一个合适的位掩码。例如,要检查整数a的第3位是否为1:
int a = 5; // 二进制: 0101
int mask = 1 << 2; // 二进制: 0100
int result = a & mask; // 结果: 0000 (即0,表示第3位为0)
高级位运算技巧
位域
位域(bit field)是一种在结构体中定义位段的小技巧。位域允许我们以更紧凑的方式存储数据。例如:
struct {
unsigned int a : 1;
unsigned int b : 3;
unsigned int c : 4;
} bitfields;
bitfields.a = 1;
bitfields.b = 5;
bitfields.c = 12;
用位运算实现加减乘除
位运算可以用来实现一些基本的数学运算,虽然这些实现方法通常并不常见,但它们可以提高某些情况下的性能。
加法
加法可以通过位运算来实现,使用按位异或和按位与运算:
int add(int x, int y) {
while (y != 0) {
int carry = x & y;
x = x ^ y;
y = carry << 1;
}
return x;
}
乘法
乘法可以通过位移和加法来实现:
int multiply(int x, int y) {
int result = 0;
while (y != 0) {
if (y & 1) {
result = add(result, x);
}
x <<= 1;
y >>= 1;
}
return result;
}
除法
除法可以通过减法和位移来实现:
int divide(int dividend, int divisor) {
int sign = ((dividend < 0) ^ (divisor < 0)) ? -1 : 1;
dividend = abs(dividend);
divisor = abs(divisor);
int quotient = 0;
while (dividend >= divisor) {
dividend -= divisor;
quotient++;
}
return sign * quotient;
}
12.4位运算注意事项
在深入探讨位运算的注意事项之前,我们先简要回顾一下位运算的基本概念和常见操作。
1 二进制和位
计算机内部使用二进制系统表示数据。二进制系统只有两个数字:0和1。每个数字称为“位”(bit)。一个字节(byte)由8个位组成,因此可以表示从0到255的值。
2 位运算符
C语言提供了几种位运算符,用于直接操作二进制位。以下是C语言中的位运算符:
按位与:&
按位或:|
按位异或:^
按位取反:~
左移:<<
右移:>>
这些运算符能够对数据的每个位进行直接操作,从而实现高效的数据处理。
3常见位运算操作和注意事项
按位与(&)
按位与运算会将两个操作数的对应位进行比较,如果两个位都是1,则结果位为1,否则为0。
注意事项:
用于位掩码:按位与常用于位掩码操作,用于清除某些位或检查某些位是否为1。在使用位掩码时,需要确保掩码的正确性,否则可能导致意外的结果。
避免混淆与逻辑与:按位与&与逻辑与&&不同,后者用于布尔逻辑操作。要避免在需要逻辑与的地方使用按位与。
int a = 5; // 二进制: 0101
int b = 3; // 二进制: 0011
int c = a & b; // 结果: 0001 (即1)
按位或(|)
按位或运算会将两个操作数的对应位进行比较,如果两个位中有一个是1,则结果位为1,否则为0。
注意事项:
用于设置位:按位或常用于设置某些位为1。在使用时,需要确保只设置需要的位,避免误操作。
避免混淆与逻辑或:按位或|与逻辑或||不同,后者用于布尔逻辑操作。要避免在需要逻辑或的地方使用按位或。
int a = 5; // 二进制: 0101
int b = 3; // 二进制: 0011
int c = a | b; // 结果: 0111 (即7)
按位异或(^)
按位异或运算会将两个操作数的对应位进行比较,如果两个位不相同,则结果位为1,否则为0。
注意事项:
用于切换位:按位异或常用于切换某些位(0变1,1变0)。在使用时,需要确保切换的位是需要操作的部分。
避免误用:按位异或^有时会被误认为是幂运算符。在C语言中,幂运算需要使用库函数pow。
int a = 5; // 二进制: 0101
int b = 3; // 二进制: 0011
int c = a ^ b; // 结果: 0110 (即6)
按位取反(~)
按位取反运算会将操作数的每个位进行翻转,即0变为1,1变为0。
注意事项:
符号位处理:对于有符号数,按位取反会影响符号位,可能导致负数的产生。在处理有符号数时需要特别小心。
应用场景:按位取反主要用于生成位掩码或快速求补运算。在实际应用中需要确保操作数的正确性。
int a = 5; // 二进制: 00000000 00000000 00000000 00000101
int b = ~a; // 结果: 11111111 11111111 11111111 11111010 (即-6)
2.5 左移(<<)
左移运算会将操作数的位向左移动指定的位数,右侧补0。
注意事项:
溢出风险:左移操作可能导致溢出,尤其是在移位数大于或等于数据类型的位数时。需要确保移位数在合理范围内。
符号位处理:对于有符号数,左移操作不会改变符号位,但可能导致数值变化。在处理有符号数时需要特别小心。
int a = 5; // 二进制: 00000101
int b = a << 1; // 结果: 00001010 (即10)
2.6 右移(>>)
右移运算会将操作数的位向右移动指定的位数,左侧补0或符号位(取决于操作数是有符号数还是无符号数)。
注意事项:
算术右移和逻辑右移:对于有符号数,右移操作可能是算术右移(保留符号位)或逻辑右移(填充0)。在不同编译器和平台上可能有所不同,需要仔细确认。
避免负数操作:对于有符号数,右移操作可能会导致意外的结果,尤其是负数。因此在右移有符号数时需要特别小心。
int a = 5; // 二进制: 00000101
int b = a >> 1; // 结果: 00000010 (即2)
4 应用场景和实践
位掩码的使用
位掩码在位运算中非常常用,通常用于清除、设置或切换特定位。
注意事项:
正确设计掩码:位掩码的设计需要符合操作需求,避免误操作。例如,要清除某些位时,掩码需要取反。
避免重复操作:在使用位掩码时,避免重复操作相同的位,可能导致不可预期的结果。
int a = 5; // 二进制: 0101
int mask = 1 << 2; // 二进制: 0100
a = a | mask; // 设置第3位为1,结果: 0111 (即7)
位域的使用
位域(bit field)是一种在结构体中定义位段的小技巧。位域允许我们以更紧凑的方式存储数据。
注意事项:
对齐问题:位域在不同编译器和平台上可能有不同的对齐方式,使用时需要注意避免对齐问题导致的数据错乱。
可移植性:位域的实现细节在不同编译器上可能有所不同,使用位域时需要特别注意程序的可移植性。
struct {
unsigned int a : 1;
unsigned int b : 3;
unsigned int c : 4;
} bitfields;
bitfields.a = 1;
bitfields.b = 5;
bitfields.c = 12;
用位运算实现数学运算
位运算可以用来实现一些基本的数学运算,虽然这些实现方法通常并不常见,但它们可以提高某些情况下的性能。
注意事项:
复杂性:用位运算实现数学运算可能增加代码的复杂性,阅读和维护难度较大。在性能不敏感的情况下,优先选择更易理解的实现方式。
正确性:确保位运算实现的数学运算结果正确,避免因位运算错误导致的计算错误。
加法
int add(int x, int y) {
while (y != 0) {
int carry = x & y;
x = x ^ y;
y = carry << 1;
}
return x;
}
乘法
int multiply(int x, int y) {
int result = 0;
while (y != 0) {
if (y & 1) {
result = add(result, x);
}
x <<= 1;
y >>= 1;
}
return result;
}
除法
int divide(int dividend, int divisor) {
int sign = ((dividend < 0) ^ (divisor < 0)) ? -1 : 1;
dividend = abs(dividend);
divisor = abs(divisor);
int quotient = 0;
while (dividend >= divisor) {
dividend -= divisor;
quotient++;
}
return sign * quotient;
}
5 性能优化
避免多余的位运算
在进行位运算时,尽量避免多余的操作,减少不必要的计算量。例如,在循环中避免重复计算相同的位掩码。
注意事项:
缓存计算结果:对于需要重复使用的位掩码或移位结果,尽可能缓存计算结果,避免重复计算。
优化逻辑:合理设计位运算逻辑,尽量减少不必要的操作,提高执行效率。
使用内置函数
一些编译器提供了内置的位运算相关函数,这些函数通常经过优化,性能优于手写的位运算代码。例如,GCC提供了__builtin_popcount用于计算二进制数中1的个数。
注意事项:
了解编译器支持:在使用内置函数前,需要了解所使用编译器是否支持这些函数,并确保程序的可移植性。
合理使用:在性能敏感的场合,优先考虑使用内置函数进行优化,但在一般场合下,保持代码的可读性更为重要。
避免位运算陷阱
在使用位运算时,需要特别注意一些常见的陷阱和误区,避免因误操作导致的程序错误。
注意事项:
操作数类型:确保位运算的操作数类型一致,避免因类型不匹配导致的位运算错误。例如,有符号数和无符号数混用可能导致不可预期的结果。
边界条件:在进行移位操作时,注意边界条件,避免移位数超出数据类型的位数。例如,对于32位整数,移位数应在0到31之间。
溢出和下溢:位运算可能导致数据溢出或下溢,需要特别注意操作数范围,确保结果在合理范围内。
6 调试和测试
位运算的调试和测试相对复杂,需要特别注意一些细节,确保程序的正确性。
使用调试工具
在调试位运算代码时,可以借助调试工具,如GDB,查看每一步操作的结果,确保运算过程符合预期。
注意事项:
逐步调试:逐步调试位运算代码,查看每一步操作的结果,确保每个位的操作正确。
观察变量值:使用调试工具观察变量的二进制表示,确保位运算结果符合预期。
编写单元测试
编写单元测试对位运算代码进行全面测试,确保每种情况都能正确处理。
注意事项:
覆盖所有情况:单元测试应覆盖所有可能的情况,包括边界条件和特殊情况,确保位运算代码的健壮性。
使用断言:在单元测试中使用断言,验证每一步操作的结果,确保程序逻辑正确。
总结
位运算在C语言中是一项非常强大的技术,能够高效地处理数据。然而,在使用位运算时需要特别注意一些细节,避免因误操作导致的程序错误。本文详细介绍了位运算的基本概念、常见操作、应用场景和注意事项,并提供了一些性能优化和调试测试的建议。希望对你在使用C语言进行位运算时有所帮助。
12.5 小结
位运算是C语言中非常强大的工具,能够高效地进行数据处理。在本文中,我们详细介绍了位运算的基本概念、常见操作和实际应用,并展示了一些高级技巧。熟练掌握位运算,可以帮助你在编写高效、紧凑的代码方面大有裨益。希望本文对你在学习和使用C语言位运算方面有所帮助。