C语言课程回顾:十二、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语言又提供了一种数据结构,称为“位域”或“位段”。
所谓“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

  1. 位域的定义和位域变量的说明
    位域定义与结构定义相仿,其形式为:
    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位。
对于位域的定义尚有以下几点说明:

  1. 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。
    例如:
struct bs
     {
       unsigned a:4
       unsigned :0        /*空域*/
       unsigned b:4       /*从下一单元开始存放*/
       unsigned c:4
     }
在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。
  1. 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。
  2. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:
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语言位运算方面有所帮助。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值