位运算的性质

1、位运算的基础知识
位运算跟二进制联系非常紧密,二进制这个概念相信大家都不陌生,我们的位运算也就是在这些0或1上进行操作。不要说二进制你都不知道。比如:
如果用一个字节来表示整数(1个字节是8位):
7的8位二进制为: 0000 0111
如果用四个字节来表示整数(4个字节是32位):
7的32位二进制为: 0000 0000 0000 0000 0000 0000 0000 0111
 
常用位运算符:& (按位与)、| (按位或)、^ (按位异或)、~ (按位取反)、>> (按位右移)、<< (按位左移)。
 
& (按位与): 二进制上按每一位(0或1)进行与运算。
先看一个8位二进制的例子:
7 & 8  = 0000 0 111 & 0000 1000 = 0000 0000 = 0
7 & 3  = 0000 0111 & 0000 0011 = 0000 0011 = 3
 
| (按位或): 二进制上按每一位(0或1)进行或运算。
7 | 8  = 0000 0 111  | 0000 1000 = 0000 1111 = 15
7 | 3  = 0000 0111  | 0000 0011 = 0000 0111 = 7
 
^(按位异或): 二进制上按每一位(0或1)进行异或运算。异或运算简单讲就是相同就为假,不同为真。
7 ^ 3  = 0000 0111  ^ 0000 0011 = 0000 0100 = 4
 
~(按位取反): 概念上来讲就是二进制上按每一位(0或1)进行取反运算。 取反运算简单讲就是0变1,1变0。
~7  = ~0000 0111 = 1111 1 000 = 0xf8 = 248 (无符号)
 
>>(按位右移): 概念上来讲就是二进制上按每一位(0或1)进行右移运算。 右移运算简单讲就是将二进制的位整体向右移动。
7 >> 2 = 0000 0111 >> 2  = 0000 0001 = 1 // 这里向右移动了2位,最低位的两个1被抹去。
这里右移两位等于除了2的2次方,7/4 = 1 在整数除法中则看成是被舍掉了小数部分。
 
<<(按位左移): 与上面右移方向的相反。
14 << 2=56,因为 14 (即二进制的 00001110)向左移两位等于 56 (即二进制的 00111000)。
 
2、实际应用:
2.1、24位颜色
我们都知道颜色(RGB),24位颜色。也就是RGB中的R(红)、G(绿)、B(蓝)分别占8位。前面的基本数据类型里,没有3字节24位的类型啊,?
 
我们用一个32位的无符号整数,高8位置零。低24位用于表示颜色。低24位怎么表示?我们都知道颜色通常每个分量是0~255之间,三种颜色存放在24位里怎么存?
 
typedef unsigned char BYTE;
typedef unsigned int UINT; (在vc编译器中32位4字节表示一个整数)
BYTE r = 255; (1111 1111)或0xff;
BYTE g = 255;
BYTE b = 255;
我们将三个分量都定成是255,这里的目的是想表示白色。
UINT color = ( r << 16 ) | ( g << 8 ) | b;
然后这样就组成了我们的颜色:白色。
 
原理很简单:
0000 0000 1111 1111 1111 1111 1111 1111
这里的颜色分量我都标识了字体的颜色,看红色的部分是不是就是左移了16位,绿色左移8位,具体的过程就是:
r << 16
0000 0000 1111 1111 0000 0000 0000 0000
g << 8
0000 0000 0000 0000 1111 1111 0000 0000
b
0000 0000 0000 0000 0000 0000 1111 1111
 
然后看这3个二进制数按位或运算后就是我们的目标颜色,用十六进制看就是:0x00ffffff 。0xff就是255。
 
另外的高24位表示法:
32位的颜色只是比24位颜色多了一个分量,可以用来做透明。也就是我们上面没有用到的最高8位。32位也可以将高8位的分量放在低8位,RGB放在高24位。比如:
1111 1111 1111 1111 1111 1111 1111 1111
 
现在仍旧回到低24位表示法:
我们知道了color,那么要取得分一个分量怎么办呢?很简单:
BYTE r = ( color >> 16 ) & 0xff;
BYTE g = ( color >> 8 ) & 0xff;
上面三句相当于逆运算。那么这里按位与上一个0xff的原理是什么呢? 我们看g分量:
color >> 8
0000 0000 0000 0000 1111 1111 1111 1111
0xff
0000 0000 0000 0000  0000 0000  1111 1111
两者相与,是不是就将红色分量给去掉了呢?
0000 0000 0000 0000 0000 0000 1111 1111
就只剩下绿色的8个1了。
 
2.2、16位颜色
再来看16位色的RGB565, 字面上的意思很简单就是r和b占5位, g占6位。一共是16位。如果是16位我们就不需要一个UINT了,只需要:
(注意短整形在vc编译器中用2个字节16位来表示)
typedef unsigned short  UINT16;
BYTE r = 255;
BYTE g = 255;
BYTE b = 255;
UINT16 color16 = ( ( r & 0xf8 ) << 8 ) | ( ( g & 0xfc ) << 3 ) | ( ( b & 0xf8 ) >> 3 ); 
0xf8就是1111 1000,和该数按位与,就是抛掉低3位;
下面:我们还是一步一步来说明吧。
 
因为是“565”模式的颜色,那么r要抛弃掉低3位,只需要高5位。g需要抛弃掉低2位,只要高6位,b和r相同,也抛弃低3位。一共加起来就是16位了。那么要把这16位分别保存这3个分量。同样是按位或运算。
 
r只剩下高5位,要到UINT16的最高5位,所以需要左移8位。
0000 0000 1111 1000   // 很明显需要向左移动8位,结果1111 1000 0000 0000。
 
同样g分量被抛弃掉低2位后:
1111 1 000 1111 1100  // 很明显需要向左移动3位,结果1111 1 111 1110 0000。
 
而b分量:
1111 1 111 1 11 1 1111   // 很明显需要向右移动3位
就是将:0000 0000 1111 1000向右移动3位后变成0000 0000 0001 1111:
上面的抛弃掉低位的算法。正因为有抛弃,因此16位颜色就没有24位颜色真实。
 
3、按位取反和按位异或
在实际的工作中,通常会有一些状态需要表示。我们这些状态又想节约一点空间。于是我们选择了用一个32位的无符号整数来存放这些状态。比如:
在游戏里面,某个玩家的一些状态也就是我们经常说的BUFF,比如:持续加血,持续加蓝,持续加体力,经脉受伤,被点穴等等。于是我们就有一个枚举:
 
enum EPLAYER_STATE
{
    EPST_NONE    = 0x00000000,     // 没有状态
    EPST_ADDHP  = 0x00000001 ,    // 加血
    EPST_ADDMP  = 0x00000002,     // 加蓝
    EPST_ADDSP   = 0x00000004,    // 加体力
    EPST_JMDAM   = 0x00000008,    // 经脉受伤
    EPST_DIANX    = 0x00000010,    // 被点穴
    EPST_XUANY    = 0x00000020,    // 被眩晕
    EPST_ATTCK    = 0x00000040,    // 被攻击
    // ... ...
    // 最多可以写32个状态,已经足够了。为什么是32,因为一个32位整数来存放的。
};
 
状态数据就定义好了,那么我们来使用它:
typedef unsigned int  UINT;
UINT dwPlayerState = EPST_NONE;
首先我们将定义的状态设置成无状态。也就是等于0。
然后,假如我们吃了一瓶子药品,我们这瓶药是用于持续加血的,因此我们就将状态设置成加血:
dwPlayerState |= EPST_ADDHP;
其它的同理。
 
假如我要同时加上几个状态的话。那么:
dwPlayerState |= ( EPST_ADDMP| EPST_ADDSP| EPST_JMDAM );
 
注意这里是|=,而不是=。因为我们不能将之前加好的EPST_ADDHP状态给抹掉了。因此要用或运算。
 
然后我们又有逻辑是用于判断我的状态里面是不是有加蓝的状态,用于如果有,我们就不能再吃蓝药了。我们就可以:
 
if ( dwPlayerState &  EPST_ADDMP )  // 判断是否这位上是否为1
{
     // 不能再吃蓝药啦。
}
 
到这里,我们又想到了。当我的蓝药持续加蓝完成后,我们应该要清除这个状态。否则就没办法再吃蓝药了。因为我们上边有检查。那我们清除状态就可以这样做:
 
if ( timeout )
{
    // 清除蓝药状态
    dwPlayerState &= ~EPST_ADDMP;   // 这样便清掉了。
    // 清除多个状态
    dwPlayerState &= ~( EPST_ADDMP| EPST_ADDSP| EPST_JMDAM );   // 这样便清掉了。
}
 
这里用到了~(按位取反)运算。~EPST_ADDMP这样的结果出来我们知道就是除了EPST_ADDMP这一位为0之外其它全部为1.然后和dwPlayerState进行按位与运算,就会把这一位给清除掉。而不影响到其它位。
EPST_ADDMP  = 0000 0000    0000 0000    0000 0000    0000 0000   0000 001 0
~EPST_ADDMP = 1111 1111    1111 1111    1111 1111    1111 1111   1111 110 1
 
这样和dwPlayerState相与,dwPlayerState中除了第二位以外的状态,只要存在(为1)就被保留下来了。第二位不管dwPlayerState中是什么,都会被清零了。就可以起到清除状态的效果了。
上面的清除几个状态也是一个道理,只不过是先将要清除的状态按位或到一起,然后统一清除。
 
那么就上面的问题,我们再来看看按位异或。
比如我要给dwPlayerState翻转两个状态,可以用异或:
dwPlayerState ^= EPST_ADDHP | EPST_ADDMP;
异或就是相同就为假,不同就为真。因此dwPlayerState ^= EPST_ADDHP | EPST_ADDMP;这句看原理:
dwPlayerState 假如为: 0000 0000  0000 0000  11 00 1 000  0000 0000  0000 1 001
EPST_ADDHP | EPST_ADDMP 为: 0000 0000  0000 0000  00 00 0 000  0000 0000    0000 0 011
上面进行异或后,很明显:
结果为: 0000 0000  0000 0000  11 00 1 000  0000 0000  0000 1 010
EPST_ADDHP、 EPST_ADDMP状态就被翻转了。
异或还有另外一个性质是:两次异或就能还原回来。
比如: a = 7, b = 8.
那么: a = a ^ b ^ b;
先看原理:
a = 0000 0111
b = 0000 1000
c = a ^ b = 0000 1111
a = c ^ b = 0000 0111
因此就此性质,我们又可以做一个不需要第三方变量,交换两个变量的值了:
a = a ^ b;   // a = 0000 1111
b = b ^ a;   // b = 0000 0111 = 7
a = a ^ b;       // a = 0000 1000 = 8
明白其中的道理了吗?其中还有个加减法的版本:
a = a + b;
b = a - b;
a = a - b;
另外再来看一些用法:
BYTE x = 6;
x = x & ( x - 1 );   // 将最右侧为1的一位给置0。x 结果位4。如果x为0,则结果为0。
原理:
6 = 0000 0110
5 = 0000 0101
x = 0000 0100
继续:
4 = 0000 0100
3 = 0000 0011
x = 0000 0000
 
利用这个性质,我们可以求一个整数中有多少位为1。
x = 6;
count = 0;
while ( x )
{
    x &= ( x - 1 );
    ++count;
}
这样便能得到多少个1,要得到多少个0就简单了: sizeof( x ) * 8 - count。原理不用说吧。
还有很多用法,比如看一个无符号整数是否为奇数,析出最右侧一位为0的那一位,析出最右侧一位为1的那一位等等。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值