【C\C++語言入門篇】-- 位運算

作者: masefee  來源: CSDN  發布時間: 2010-11-19 09:50  閱讀: 2339 次  推薦: 5   原文鏈接   [收藏]  

  回顧之前的篇幅,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 char BYTE;
  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 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了。這里我只是舉的255,因此可能有的朋友會說我直接:

  BYTE g = ( color >> 16 )& 0xff; 這樣也等于255啊。這里我是舉的一個比較特殊的例子,當這里r g b不相等的時候,就不能這樣用了,這里是通用的用法,我們不能特殊化。

  再來看16位色的RGB565,字面上的意思很簡單就是r和g占5位, b占6位。一共是16位。如果是16位我們就不需要一個UINT了,只需要:

 
typedef unsigned short UINT16;
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,比如:持續加血,持續加藍,持續加體力,經脈受傷,被點穴等等。于是我們就有一個枚舉:

 
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

  利用這個性質,我們可以求一個整數中有多少位為1。

 

 
x = 6;
count
= 0;
while (x)
{
x
&= (x- 1 );
++count;
}

 

  這樣便能得到多少個1,要得到多少個0就簡單了撒: sizeof(x)* 8 - count。原理不用說吧。還有很多用法,比如看一個無符號整數是否為奇數,析出最右側一位為0的那一位,析出最右側一位為1的那一位等等。這里就不多介紹了。大家可以結合者上面的例子擴展思路。

  好了,本文就介紹到這里。

5
0
 
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()