位运算
位运算是C/C++中基本的运算之一,它能够对一个变量或数值中的个别位进行操作,刚开始接触或许会对这种操作感到奇怪,但是在某些场合,位运算便显得必要、高效以及简洁。虽说位运算是基本的运算,但我们平时编程的时候却很少去运用它,这或许就是以致我们认为它没有实用性的原因。
我也是初次接触位运算,同样对于如何运用它觉得毫无头绪。对于应用方面,在网上看过不少实例(有解释的),但自己还是觉得摸不着头脑(自己学识还是太浅了=_=),这确实是要花上不少时间去琢磨。
接下来,本篇先介绍位运算符及其用法,然后再举几个例子,方便自己和看本篇文章的人加深认识(自己也是个初学者。。本篇写出来也是给自己和新手看的,各位大神就无视吧)。
开始接触位运算:
无论是什么类型的数据,它在计算机底层中都以二进制的形式表示,一个数据可以表示为一个有限且有一定顺序的位序列集合,每一位只能表示两种状态,0或1,这可以简单地理解为打开和关闭两种状态。只要有足够多的位,无论什么形式的数据都能被表示。如果有读者对编码感兴趣的,推荐看一下《编码的奥秘》这本书,我觉得还是比较有趣的。而位运算,是单独对数据某些位进行操作,要实现这运算,我们首先需要了解位运算符:
位运算操作符 | 功能 | 表达式 | 功能描述 |
~ | 位非 | ~exp | 将exp的每一位取反 即:0 ->1 1->0 |
& | 位与 | exp1 & exp2 | exp1与exp2对应位都为1,结果位为1,否则为0 |
| | 位或 | exp1 | exp2 | exp1与exp2对应位其中一个或都为1,结果位为1;对应位都为0,则结果位为0 |
^ | 位异或 | exp1 ^ exp2 | exp1与exp2对应位都为1或都为0,则结果位为0;对应位不都为1,则结果位为1 |
<< | 左移 | exp<<n | 将exp的所有位向左移,移出外面的将被丢弃,右边补0,这相当于将exp乘以2n |
>> | 右移 | exp>>n | 将exp的所有位向右移,移出外面的将被丢弃,左边补0,这相当于将exp除以2n |
每种位运算符的作用例子:
位非(not):~
~(00110100) /*表达式*/
(11001011) /*结果值*/
位与(and):&
(01001110)&(10010011) /*表达式*/
(00000010) /*结果值*/
位与(or):|
(10001101)|(01010011) /*表达式*/
(11011111) /*结果值*/
位异或(xor):^
(00101101)^(10110101) /*表达式*/
(10011000) /*结果值*/
左移:<<
(00000100)<< 2 /*表达式*/
(00010000) /*结果值*/
右移:<<
(00000100)>> 2 /*表达式*/
(00000001) /*结果值*/
注意点:
1、位运算只能用于整形类的数值(char short int long同时包括它们的signed、unsigned),它是不能操作浮点数(float double)
2、使用位运算时,尽量使用无符号整形作为操作数,因为有符号的整形在进行移位的时候,其结果值是依赖于其系统平台的(比如对有符号进行右移,会出现两种情况:①:在高位补0 ②在高位补符号位,所以其移植性就差了许多。
例子1——使用位运算,将数字转换为它的二进制形式并显示出来
#include<stdio.h>
char * itob(char *ps,int n);
int invert_bits(int n,int bits);
void show_bstr(const char *ps);
int main(void)
{
int num,inv_num,ib;
char binary_string[8*sizeof(int)+1];
printf("请输入一个正整数 以及你想将其最后几位取反的位数 (输入q退出) : ");
while (scanf("%d%d",&num,&ib)==2 && ib<=32)
{
printf("\n%d is ",num);
show_bstr(itob(binary_string,num));
inv_num=invert_bits(num,ib);
printf("将最后 %d 位取反得到:\n%d is ",ib,inv_num);
show_bstr(itob(binary_string,inv_num));
printf("\n请输入下一个正整数 以及你想将其最后几位取反的位数 (输入q退出)");
}
printf("Bye!!");
getchar();
return 0;
}
char *itob(char *ps,int n) /*将十进制整数n转换为它的二进制形式*/
{
int i;
int size=8*sizeof(int);
for(i=size-1;i>=0;i--,n>>=1)
ps[i]=(01&n)+'0'; //使用01或1作为掩码,查看n的最后一位是0或1
ps[size]='\0';
return ps;
}
void show_bstr(const char *ps) /*显示此整数的二进制形式*/
{
int i=0;
while (ps[i])
{
putchar(ps[i]);
if (++i%4==0 &&ps[i]!='\0') //每四个字符空一格
putchar(' ');
}
putchar('\n');
}
int invert_bits(int n,int bits) /*将整数n最后bits位取反*/
{
int mask=0;
int bit_val=1;
while (bits--) //构造一个掩码
{
mask|=bit_val;
bit_val<<=1;
}
return n^mask;
}
解析:
为了便于理解和查看,此程序将数值的每一位都放到char数组中,(for(i=size-1;i>=0;i--,n>>=1) ps[i]=(01&n)+'0'; )这里n>>=1,使得n每次都向右移动一位(除以2),接着(01&n),01作为掩码(打开最后1位),这样便能求出数值最后一位是1或是0。show_bstr()函数中,(if (++i%4==0 &&ps[i]!='\0') putchar(' ');),这使得每输出4位信息则输出一个空格。而invert_bits()函数,里面的while()循环是根据使用者的需要,构造一个最后bits位为1的掩码,最后通过整数与掩码进行异或运算,将最后的bits位转((取反)。
回顾之前的位运算符介绍,只要掩码与位异或运算符结合使用,也能把位转置(取反);如果mask=1,bit是一个位(0或1),若bit=0,则mask^bit为1,若bit=1,则mask^bit为0;如果mask=0,那bit无论为0或1,mask^bit都为bit。总的来说就是,掩码与^运算符结合使用,数值对应掩码位为1的位被转置,对应掩码为0的位保持原来状态。
例子2——交换两个整数
对于交换两个数的方法,网上有很多了,在此不一一举例,在这里主要说一些典型的例子。一般来说,当我们需要交换两个变量的数值时,一般都要引入一个临时变量作为桥梁。就像是一下的代码。
void swap_1(int *a,int *b)
{
int temp;
temp=*a;
*a=*b;
*b=temp;
}
这种引入一个临时变量用于交换两变量的数值的方法,其优点是简单易懂,能够用于所有的内置类型数据。但是,它也不是一种比较好的方法。假若(只是假若),你的程序要同时交换多个数据,那么就需要多个临时变量,这样会导致程序在临时变量销毁之前,开销会比较大。有人可能会说,这点微不足道的开销不会对程序造成什么影响,但是俗话也有说,千里之堤毁于蚁穴。一个好的程序都是由很多精巧设计的细小部分组成的。往往一些不起眼的东西,它可能就是决定性的关键所在。
现在,用一种新的方法来交换两个数,这种方法不用引入临时变量,但是这只适用于交换整形类的数值。此处,我们仅需要用到掩码以及位异或运算符就能做到不用临时变量就能交换两数值了。
方法代码如下:
void swap_2(int *a,int *b)
{
*a=*a^*b;
*b=*a^*b;
*a=*a^*b;
}
解析:
代码也只是短短3句,一眼扫过去,也看不出什么门道,但是它确真能交换两个整数。当然,编程老手可能一下子就能看出其原理来了。我是初次学习位运算,苦想了一段比较长的时间才了解其原理的一点皮毛。下面,我就以自己的理解来说明一下它为何有这功用(可能说得不够专业,我也只是尽量说得通俗一点,大家别介意)。
下面是详尽解释:
整数a=1
整数b=3
我们首先来看第一条语句 *a=*a^*b; 表达式所产生的结果将会是 a=1^3=2 ,在这里我们并不关心数值上的变化,我们要关心的是a上每一位的变化。经过第一句的运算,a的每一位如下所示:
结果值
从结果值知道,只有当a与b两个整数分别所对应的位不同时, 结果值所对应的位就会被设置为打开(1),而所对应的位相同时,结果值相对应的位就会被设置为关闭(0)。这样,我们可以间接理解为,经过两数的位异或运算后,得出的结果值是标记两数之间差异位的信息。因此,a与b的差异位是第2位。
而第二第三条语句是的功用是一样的,将a与b所拥有的值交换,也可以说是把两个值翻转。这里,我们能把a看作一个储存两数差异位的掩码,再结合位异或运算,就能达到交换两个整数的功能。前面也有讲到,掩码与位异或一起使用,掩码位为1的,将会转置该位,为0的保持不变。总结一句是,将差异位翻转
第二条语句:
*a | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
*b | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
*b=*a^*b | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
*b=*b^*a 此句表示将整数b中,把与整数a有差异的位翻转,这样b中保存的值就会转化为原整数a的值(b=1),而a中仍是保存着两数间差异位的信息。
第三条语句:
*a | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
*b | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
*a=*a^*b | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
接着最后这句 *a=*a^*b 此句,因为*b是原来整数a的值,所以此句表示将整数a中,把与整数b有差异的位翻转,这样便把记录差异位的掩码a的值转化为*b中的值(即原整数a的值)。
综观上述解析,其实不难看出位异或是一种自反运算,可以互逆的(自身进行异或运算是自身相反,在这基础上再进行一次异或运算便能得到自己本身)。这就像加法与减法是互逆的,其实,你立马能想到,可以用利用加法与减法的互逆性,实现不引入临时变量交换两个整数。但是,使用位异或实现还是比使用加减法来实现来得有优势的。使用加减法实现有两个问题:1、若使用加减法,可能会碰到溢出的问题,int char long等整数类型都是有边界的,太大或太小的数进行加减,难免会遇到溢出问题 2、使用加减法实现的效率会比使用位运算实现低一点吧(我估计=_=)。
好了,我就写这么多了。整篇文章像流水账似的,我经验还不足,大家就别介意了,不过细节方面比较清楚的 希望各位看完能够对位运算有一定的了解,我以后会写出更好的文章的:D .