【C\C++語言入門篇】-- 位運算
回顧之前的篇幅,C語言的主體部分基本已經介紹完了。之所以沒有介紹C++的相關特性是因為在之前的文章中C和C++在這些方面都有共性,所以在面向對象之前。我們先把這些共性給介紹完。也就是說在介紹面向對象之前,所有的文章都是CC++中都能使用的。從這點上來看,現在正極力奮斗于C++戰線上的初學者還是很有用處的。
本篇繼續沿著這條路線,到本篇為止包括本篇都還不會急于去介紹C++的面向對象的特性。那么在之前的文章中,可以說基本都把內容給介紹完了。本篇雖然不是大概念,但是在實際的項目中是絕對離不開的。那么我們就在本篇開始我們的位運算旅程。
首先,位運算到底用來做什么,用處多不,好像到現在我也沒有怎么用位運算呢?很多初學者我相信會有這樣的疑問。那么本篇就將介紹位運算的強大用途及無限魅力。
位運算跟二進制聯系非常緊密,二進制這個概念相信大家都不陌生,我們的位運算也就是在這些0或1上進行操作。不要說二進制你都不知道。比如:
7的8位二進制為: 0000 0111
7的32位二進制為: 0000 0000 0000 0000 0000 0000 0000 0111
二進制與十進制的換算我就不說了。上面為什么三個1就表示7,不知道的話就看看書哈。
上面說到了8位和32位,我們知道一個字節(byte)表示8位,那么二進制的一位就是這個位的意思。int是32位,那么寫完整數字0的二進制就有32個0。這樣思考起來在后面的位運算上要好理解一點。先來看看我們經常用到的位運算符:& (按位與)、|(按位或)、^ (按位異或)、~(按位取反)、>> (按位右移)、<< (按位左移)。
& (按位與): 概念上來講就是二進制上按每一位(0或1)進行與運算。那么與運算是什么意思該不用我說吧,就是兩者都是1結果為真。其中一個為0結果為假。這里不可能有0、1之外的數,這里是二進制。先看一個8位二進制的例子:
7 & 8 = 0000 0 111 & 0000 1000 = 0000 0000 = 0
7 & 3 = 0000 0111 & 0000 0011 = 0000 0011 = 3
很簡單吧。不用多說了,就是操作0和1。
|(按位或): 概念上來講就是二進制上按每一位(0或1)進行或運算。那么或運算是什么意思該不用我說吧,就是兩者都是0結果為假。其它情況都為真。
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 在整數除法中則看成是被舍掉了小數部分。
<<(按位左移): 這個就不說了,與上面右移方向的相反。
好了,有了基本的概念。那么下面就進入實際應用了。
我們都知道顏色,比如你再惹我。我就給你顏色看看。那么這里的顏色就是RGB,我們在這里談24位顏色。也就是RGB中的R(紅)、G(綠)、B(藍)分別占8位。這下有的朋友疑惑了,24位?想想前面的基本數據類型里,沒有24位的類型啊,怎么辦呢?
于是,我們便用到了位運算。一個32位的無符號整數,高8位置零。低24位用于表示顏色,到這里又有朋友想了。低24位怎么表示?我們都知道顏色通常每個分量是0~255之間,三種顏色存放在24位里怎么存?
typedef unsigned int UINT;
BYTE r = 255;
BYTE g = 255;
BYTE b = 255;
我們將三個分量都定成是255,這里的目的是想表示白色。
UINT color = ( r << 16 )|( g << 8 )| b;
然后這樣就組成了我們的顏色:白色。
那么這里的原理很簡單:
0000 0000 1111 1111 1111 1111 1111 1111
這里的顏色分量我都標識了字體的顏色,看紅色的部分是不是就是左移了16位,其他同理,具體的過程就是:
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個二進制數按位或運算后就是我們的目標顏色,用十六進制看就是:0x00ffff ff 。0xff就是255。
32位的顏色只是比24位顏色多了一個分量,可以用來做透明。也就是我們上面沒有用到的最高8位。32位也可以將高8位的分量放在低8位,RGB放在高24位。比如:
1111 1111 1111 1111 1111 1111 1111 1111
現在我們知道了color,那么要取得分一個分量怎么辦呢?很簡單:
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了。這里我只是舉的255,因此可能有的朋友會說我直接:
BYTE g = ( color >> 16 )& 0xff; 這樣也等于255啊。這里我是舉的一個比較特殊的例子,當這里r g b不相等的時候,就不能這樣用了,這里是通用的用法,我們不能特殊化。
再來看16位色的RGB565,字面上的意思很簡單就是r和g占5位, b占6位。一共是16位。如果是16位我們就不需要一個UINT了,只需要:
BYTE r = 255;
BYTE g = 255;
BYTE b = 255;
UINT16 color16 = (( r & 0xf8 )<< 8 )|(( g & 0xfc )<< 3 )|(( b & 0xf8 )>> 3 );
天啊,有的朋友可能看到這一串就暈了,其實我們碰到這種問題,如果對十六進制數不敏感不熟悉的話你就用WINDOWS自帶的計算器進行算嘛。我們還是一步一步來說明吧。
因為是“565”模式的顏色,那么r要拋棄掉低3位,只需要高5位。g需要拋棄掉低2位,只要6位,b和r相同,也拋棄低3位。一共加起來就是16位了。那么要把這16位分別保存這3個分量。同樣是按位或運算。r只剩下高5位,要到UINT16的最高5位,所以需要左移8位。
0000 0000 1111 1000 //很明顯需要向左移動8位
同樣b分量被拋棄掉低2位后:
1111 1 000 1111 1100 //很明顯需要向左移動3位
而b分量:
1111 1 111 1 11 1 1111 000 //很明顯多出兩個0需要向右移動3位
上面的拋棄掉低位的算法不用說了吧,不熟悉的就用計算器算相與后是不是想要的結果。正因為有拋棄,因此16位顏色就沒有24位顏色真實。
問題一:為什么要拋棄低位,不拋棄高位?(比如紅色就可以是:r & 0x1f)
上面24位色反過來逆運算獲得每一個分量我們已經知道了,那么:
問題二:怎么獲得RGB565顏色color16中的每一個分量。
上面的顏色了解后,我相信大家對于& |<< >>這幾個該沒有什么問題了吧,當然顏色的組合還有其他的,這里不是為了介紹顏色。而是為了了解位運算。位運算很靈活,這里只是一個基本的介紹。更多的還需要大家多實踐。
了解了上面的幾個運算符,下面介紹剩下的兩個:按位取反和按位異或。
在實際的工作中,通常會有一些狀態需要表示。我們這些狀態又想節約一點空間。于是我們選擇了用一個32位的無符號整數來存放這些狀態。比如:在游戲里面,某個玩家的一些狀態也就是我們經常說的BUFF,比如:持續加血,持續加藍,持續加體力,經脈受傷,被點穴等等。于是我們就有一個枚舉:
{
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位整數來存放的。
};
狀態數據就定義好了,那么我們來使用它:
UINT dwPlayerState = EPST_NONE;
首先我們將定義的狀態設置成無狀態。也就是等于0。然后,假如我們吃了一瓶子藥品,我們這瓶藥是用于持續加血的,因此我們就將狀態設置成加血:
其它的同理。假如我要同時加上幾個狀態的話。那么:
注意這里是|=,而不是=。因為我們不能將之前加好的EPST_ADDHP狀態給抹掉了。因此要用或運算。然后我們又有邏輯是用于判斷我的狀態里面是不是有加藍的狀態,用于如果有,我們就不能再吃藍藥了。我們就可以:
{
//不能再吃藍藥啦。。
}
到這里,我們又想到了。當我的藍藥持續加藍完成后,我們應該要清除這個狀態。否則就沒辦法再吃藍藥了。因為我們上邊有檢查。那我們清除狀態就可以這樣做:
{
//清除藍藥狀態
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 假如為: 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; 先看原理:
b = 0000 1000
c = a ^ b = 0000 1111
a = c ^ b = 0000 0111
因此就此性質,我們又可以做一個不需要第三方變量,交換兩個變量的值了:
b = b ^ a; // b = 0000 0111 = 7
a =a ^ b; // a = 0000 1000 = 8
明白其中的道理了嗎?其中還有個加減法的版本:
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
利用這個性質,我們可以求一個整數中有多少位為1。
count = 0;
while (x)
{
x&= (x- 1 );
++count;
}
這樣便能得到多少個1,要得到多少個0就簡單了撒: sizeof(x)* 8 - count。原理不用說吧。還有很多用法,比如看一個無符號整數是否為奇數,析出最右側一位為0的那一位,析出最右側一位為1的那一位等等。這里就不多介紹了。大家可以結合者上面的例子擴展思路。
好了,本文就介紹到這里。