2.10 ビット処理
2.10.1 ビット単位の演算
算術演算は数学的な計算を行うのに適していますが、2進数を制御するには適しているとはいえません。つまり、ビット単位での計算を行う場合、算術演算は不向きです。例えば、32ビットの整数型変数から上位8ビットを取得したいという場合、算術演算ではなくビット単位の計算が必要になります。そこで、これらのビット制御を行う場合はビットごとの論理演算を行います。
演算子 | 意味 |
---|---|
& | ビットごとの論理積 |
| | ビットごとの論理和 |
^ | ビットごとの排他的論理和 |
~ | 1の補数 |
&= | ビットごとの AND 代入 |
|= | ビットごとの OR 代入 |
^= | ビットごとの排他的 OR 代入 |
表1の演算子をビット処理演算子と呼びます。なんだか、論理積とか論理和とか、難しそうな言葉が出てきましたが複雑に考えないでください。論理演算は 2 進数の 0 と 1 をスイッチの ON と OFF のように考え、0 の状態を偽(FALSE)、1 の状態を真(TRUE)と考えます。
まずは論理積から使ってみましょう。論理積(AND)とは比較する二つのビットが双方とも 1 であれば真、そうでなければ偽を算出します。この関係は表2のようになります。
A | B | A & B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
この論理積の性質を利用すれば、特定のビットのみを抽出するということが可能となります。例えば 0101 1100 という 2 進数がある場合、下位 4 ビットの値を取得したければ 0000 1111 との論理積を求めます。
このような計算をさせれば、マスクビットの上位4ビットは 0 なので結果は必ず 0 となり、下位4ビットは全てが 1 なので、比較するビットのうち 1 の部分だけが 1 として摘出されます。この性質は、RGB 形式の色情報を示す 32 ビットの値から赤い要素だけを抽出したい場合などに適応されます。32 ビットのうち 16 ~ 23 ビットが赤い要素を表している場合は、これに 0xFF0000 との論理積を求めることで、赤い要素の値だけを取得できます。
#include <stdio.h> int main() { char ch5 = '5'; printf("ch5:%%c = %c , %%X = %X , & = %d\n" , ch5 , ch5 , ch5 & 0x0F); return 0; }
このプログラムは、ASCII 文字の数字と実際の数値の関係を知ることができます。文字定数としての 数字(例えば '5')という数字と、整定数(例えば 5)はまったく異なるものです。実際に文字定数 '5' を数値として出力すれば、まったく関係のない値であることがわかります。しかし、ASCII 文字では数字も 0 ~ 9 まで並んでおり、その規則を知ることで文字定数を実際の数値に変換させることができるようになります。
実は ASCII 文字の数字は16進数 0x30 ~ 0x39 までにマッピングされています。つまり、文字定数 '0' は ASCII コード 0x30 であり、逆の考え方をすれば ASCII コード 0x37 は文字定数 '7' であると考えることができます。この規則性を生かし、文字定数の上位4ビットの値を除去すれば、純粋な数値に変換することができるということになります。コード1はまさにこれを ch5 & 0x0F で行っています。ASCII コードの数字に対して 0x0F との論理積を求めることによって数値に変換することができるのです。
次に、論理和(OR)ですが、これはビットの一方が 1 であれば真という結果を出します。全てを 0 で比較すれば元の値が、全てを 1 で比較すれば全ビットが 1 になります。
A | B | A | B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
この性質をうまく利用すれば、ビットフラグの組み合わせを作成することができます。又、先ほどとは逆に数値から ASCII コードの数字に変換するような処理を行うことができるようになります。
#include <stdio.h> int main() { int iVar = 5; printf("iVar:%%d = %d\niVar | 0x30:%%c = %c\n" , iVar , iVar | 0x30); return 0; }
このプログラムは、数値型変数 iVar に格納されている数値を ASCII 文字の数字に変換します。ただし、iVar の値は1桁でなければなりません。
排他的論理和(XOR)は、両者の値が等しい時に偽となる論理演算で、一方が1で一方が0の時に真となります。同じビット列の排他的論理和 A ^ A は常に 0 となる性質があります。また A ^ B を行った結果 C に C ^ B を求めると、結果は A になるというような性質もあります。
A | B | A ^ B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
#include <stdio.h> int main() { int iVar1 = 0xF0F0 , iVar2; printf("iVar1 ^ iVar1 = %X\n" , iVar1 ^ iVar1); iVar2 = iVar1 ^ 0xABCD; printf("iVar2 = %X\n" , iVar2); printf("iVar2 ^ 0xABCD = %X\n" , iVar2 ^ 0xABCD); return 0; }
コード3は、変数 iVar1 に 0xF0F0 を代入しています。最初の printf() では iVar1 同士の排他的論理和を求めています。同一のビット列の排他的論理和は常に 0 となる性質があるため、この結果が 0 になることを確認できるでしょう。次に iVar2 に iVar1 と 0xABCD の排他的論理和を格納します。この値は元の値とは関係のない数値になっていますが、iVar2 にもう一度同じ値 0xABCD の排他的論理和を求めると、元の値 iVar1 と同じになります。
最後に1の補数ですが、これは単純にビット列を反転させます。~ 演算子は他のビット処理演算子と異なり単項演算氏で、与えた値のビット列を反転させます。例えば、ビットマップイメージから各ピクセルの色情報を取得し、~ 演算子を用いてビットを反転させることによって色を反転させるということも可能です。
#include <stdio.h> int main() { unsigned int iVar = 0xCC33CC33; printf("~%X = %X\n" , iVar , ~iVar); return 0; }
コード4は、符号なし整数 iVar に 0xCC33CC33 を代入しています。これは2進数で 1100 1100 0011 0011 1100 1100 0011 0011 という32ビット値ですが、これを反転させると 0011 0011 1100 1100 0011 0011 1100 1100 すなわち16進数 0x33CC33CC という値になります。このプログラムを実行すれば ~ 演算子によってビットが反転していることを確認できるでしょう。
2.10.2 ビットシフト
ビットシフトは、ビット列をそのまま左、又は右へ移動させる演算です。ビットシフトにはシフト演算子を用いて行います。シフト演算子とシフト演算を用いた複合代入演算子は表5 のようなものがあります。
演算子 | 意味 |
---|---|
<< | 左シフト |
>> | 右シフト |
<<= | 左シフト代入 |
>>= | 右シフト代入 |
シフト演算子は、次のように使用します。
値 << シフト数 値 >> シフト数
<< はビットを左にシフトし、>> はビットを右にシフトします。値をいくつシフトするかは、シフト数で指定します。シフトしたことでできた空白は0で埋められます。
1111 1111 >> 2 --右に2シフト--> 0011 1111
このような機能、いったい何に使うんだろうと思うかもしれません。じつは、右にシフトするたびに2で除算した結果と同じになり。 左にシフトするたびに2で乗算したことと同じになります。一般的にCPUは算術演算よりもシフト演算を用いた方が処理が速いというメリットがあります(コンパイラによっては、コンパイル時に最適化してしまうため、変わらないこともあります)。他にも32ビット列を24ビット右シフトすることによって、上位8ビットを取得するというような利用方法もあります。
#include <stdio.h> int main() { int iVar = 100; printf("iVar / 4 = %d\niVar * 4 = %d\n" , iVar >> 2 , iVar << 2); return 0; }
このプログラムは、100 という値を持つ変数に対して右に2シフトした値と、左に2シフトした値を表示します。2回シフトするということは、4で乗算、又は除算するということに等しいと考えられます。
データ型のサイズを超えるシフトを行った場合は切り捨てられます。また、 符号付の整数を右シフトした場合は、最上位ビットが変化してしまうため符号が変わってしまいそうですが、実は右シフトにおいて符号は保存されることもあります。符号付整数を右シフトした場合は、最上位ビットは保存された状態でシフトされ、符号なし整数を右シフトした場合は、最上位ビットがクリアされます。これを算術シフトと呼びます。逆に、どのような状態であれ最上位ビットが常に 0 でクリアされるシフトを論理シフト と呼びます。算術シフトが行われるか、論理シフトが行われるかは実装に依存します。そのため、移植性の高いプログラムを記述することが目的の場合は、算術シフトや論理シフトなどの結果に依存しないように注意しなければなりません。
#include <stdio.h> int main() { int iVar = -100; printf("iVar >> 2 = %d\n" , iVar >> 2); return 0; }
コード6は、-100 の値を持つ符号付整数型変数 iVar に対して右シフトを行っていますが、算術シフトが行われるか、論理シフトが行われるかはプログラムを実行する計算機によって異なります。