位运算一直都是初学C语言的难点内容,虽然只有与、或、异或、非、左移和右移6种操作,但是它们的搭配使用才是最常见的,单纯的使用一种为运算还是比较少且简单的情况。举例来说,“不用加法实现加法”、“不用减法实现减法”、“求一个整数的二进制中1的个数”、“实现bitmap”……
下面是我以C语言为基础总结的位运算的一些细小知识点,后面附有上述问题的解决代码,希望对初学C语言的朋友们有一点帮助。
1. 位运算符中除了~以外,均为二目运算符,即要求两侧各有一个运算量。运算量只能是整型或字符型的数据,不能为实型数据。
2. 位运算都是对补码进行运算。
3. 按位与(&)运算符的作用
a) 清零。将待清零位与0进行按位与即可。
b) 取一个数中某些指定位或保留指定位。将指定位与1进行按位与,而其他位与0按位与即可。
c) 统计整型数据二进制位中1的个数(见后附代码)
d) 判断奇偶
Int a = 7, b = 0;
b = a & 1; //结果为1则为奇数,为0则为偶数
4. 按位或(|)运算符的作用
a) 指定某些位为1。将指定位与1进行按位或即可
5. 按位异或(^)运算符的作用
a) 使特定位翻转。将特定位与1进行按位异或即可。
b) 与0异或保留原值。
c) 当一组数据中只有一个数据有奇数个,其他数据都有偶数个,找到这个有奇数个的数据
int nArray[]={11,22,33,11,22,44,55,22,33,44,55,22,33,55,55};
intnResult = 0;
intnSize = sizeof(nArray)/sizeof(nArray [0]);
int I = 0;
for(i=0; i< nSize; ++i)
{
nResult = nResult ^ nArray [i];//自己和自己异或为0,0再与其他数据异或,其他数据保持不变,最后就只剩下奇数个的数据被保留在nResult中
}
printf("%d\n", nResult);//输出33
d) 交换两个值,不用临时变量。
例如:intnValue1 = 3,;
intnValue2 = 4;
nValue1 = nValue1 ^ nValue2;
//相当于nValue2= nValue1 ^ nValue2 ^ nValue2 = nValue1 ^ 0 = nValue1
nValue2 = nValue1 ^ nValue2;
//相当于nValue1= nValue1^ nValue2^ nValue1= nValue2^0 = nValue2
nValue1 = nValue1 ^ nValue2;
即最后交换了nValue1和nValu2的值。
当然,也可以不用位操作和临时变量
如:intnValue1 = 3,;
intnValue2 = 4;
nValue1 = nValue1 + nValue2;
//相当于nValue2= nValue1 +nValue2 - nValue2 =nValue1
nValue2 = nValue1 - nValue2;
//相当于nValue1= nValue1+ nValue2- nValue1 = nValue2
nValue1 = nValue1 - nValue2;
6. 按位取反(~)运算符的作用
a) 将数据最后一位置零
例如:a= a&~1 //~的优先级大于&的优先级
b)对一个整型数据按位取反,所有位都进行了取反操作,包括符号位。(如:inta = -1;a= ~a;结果a的值为0)
7. 左移运算符(<<)和右移运算符(>>)
a) 左移1位相当于该数乘以2^1,左移n位相当于该数乘以2^n。但此结论只适用于该数左移时被溢出舍弃的高位中不包含1的情况。左移时高位溢出舍弃,低位补0。
b) 右移时,如果是无符号类型,低位溢出舍弃,高位补0;如果是有符号类型,低位溢出舍弃,高位补充符号位(即正数补0,负数补1)。此外,对于不同的系统来说,当符号位为1,右移时,有的系统在高位补0,有的系统在高位补1。补0的称为“逻辑右移”,即简单右移;补1的称为“算术右移”。
c) 循环右移
intLoopRight(int a, int n)//数据a右移n位,低位溢出的n位放到高位前n位
{
int i,j;
i = (unsigned int)a >> n;//a右移n位高位前n位为0
j = a << (32 - n);//a左移(32-n)位,将低n位的数据移到高位的前n位处
return i | j;//按位或运算后,即将低位溢出的n为放到了高位的前n位处。因为,i高位的前n位为0,其他位为a中原来的数据,保持不变,而j高位的前n位是a溢出的低n位,其他位为0.
}
8. 如果两个数据长度不同(例如long型占8个字节和int型占4个字节)进行位运算时(如a& b,而a为long型,b为int型),系统会将二者按右端对齐。如果b为正数,则左端32位补满0;若b为负数,左端应补满1;如果b为无符号整型,则左端补满0。
9. 位段
a) 位段成员的类型必须指定位unsigned或int类型。
例如:struct packed_data
{
unsigned a:2;
unsigned b:6;
unsigned c:7;
unsigned d:2;
int I;
};
packed_datadata;
data.a= 2;
注意:a占2位,最大值为3.如果大于3将自动取赋予它的数的低位。即如果给data.a赋8,data.a的值将为0.
b) 若某一位段要从另一个字开始存放,可以用以下形式定义:
unsigneda:2;
unsignedb:6;
unsigned:0;
unsignedc:2;
本来a、b、c应连续存放在一个存储单元(字)中,由于用了长度为0的位段,其作用是使下一个位段从下一个存储单元开始存放。因此,现在只将a、b存储在一个存储单元中,c另存放在下一个单元(上述“存储单元”可能是一个字节,也可能是两个字节,视不同的编译系统而异)。
c) 一个位段必须存储在同一存储单元中,不能跨两个单元。如果第一个单元空间不能容纳下一个位段,则该空间不用,而从下一个单元起存放该位段。
d) 可以定义无名位段
例如:unsigneda:2;
unsignedb:6;
unsigned:6;//这6位空间空着,不用来存储数据
unsignedc:2;
e) 位段的长度不能大于存储单元的长度,也不能定义位段数组。
f) 位段可以用整型格式符输出。
例如:printf(“%d,%d,%d\n”,data.a, data.b, data.c);
当然,也可以用%u、%o、%x等格式符输出。
g) 位段可以在数值表达式中引用,它会被系统自动地转换成整型数据。
程序实例
#include
//函数声明
int Add(int nValue1, int nValue2);
int Sub(int nValue1, int nValue2);
int Avg(int nValue1, int nValue2);
int NumOf1(int nValue);
int main (int argc, char **argv)
{
intnValue1 = 0;
intnValue2 = 0;
printf("请输入参加运算的两个整数\n");
printf("nValue1:");
scanf("%d",&nValue1);
printf("nValue2:");
scanf("%d",&nValue2);
printf("%d+ %d = %d\n", nValue1, nValue2, Add(nValue1, nValue2));
printf("%d- %d = %d\n", nValue1, nValue2, Sub(nValue1, nValue2));
printf("(%d+ %d)/2 = %d\n", nValue1, nValue2, Avg(nValue1, nValue2));
printf("%d的二进制中1的个数为:%d\n", nValue1, NumOf1(nValue1));
return0;
}
//加法
int Add(int nValue1, int nValue2)
{
intnSum = 0;
intnCarry = 0;
do
{
nSum= nValue1 ^ nValue2;//将二进制每位相加的结果保存到nSum中(不考虑进位)
nCarry= (nValue1 & nValue2) << 1;//将进位保存到nCarry中
nValue1= nSum;
nValue2= nCarry;
}while(0!= nCarry);//循环计算,直到进位为0
returnnSum;
}
//减法
int Sub(int nValue1, int nValue2)
{
returnAdd(nValue1, ~nValue2+1);
}
//求两个数的平均数
int Avg(int nValue1, int nValue2)
{
return(((nValue1 ^ nValue2) >> 1) + (nValue1 & nValue2));
}
//计算二进制1的个数
int NumOf1(int nValue)
{
intnCount = 0;//计数器
while(nValue)
{
++nCount;
nValue= (nValue - 1) & nValue;//每次将二进制最左边的1变为0
}
returnnCount;
}