こんにちは、ナナです。
2進数と16進数を学んだところでビット演算と呼ばれる演算方法を学んでみましょう。
進数について詳しく知りたい方は『C言語 2進数 16進数 考え方と変換方法【0と1で数を表現?】』の記事を参照してください。
ビット演算の方法が書かれたサイトはありますが、どういう場面でどのように使うのかという実践的なシーンに言及しているものは少ないです。
本記事では次の疑問点を解消する内容となっています。
では、ビット演算の使い方を学んでいきましょう。
ビット演算を学ぶ前にビットを知ろう
師匠!数値情報を操るための「ビット演算」と呼ばれる傀儡の術があると聞いたことがあります。そろそろ、私のチャクラ量もごりごり上がってきていますし、扱える時期に来たんじゃないですか?教えてくださいっ!
「ビット演算」だね。そろそろ扱ってもいい頃かもね。じゃあ、ビット演算を知る前にビットとは何なのかから学んでいこう!
ビットの概念
メモリというハードウェアは1バイトという単位で存在し、順番に記憶領域が並んでいます。1バイトとはビット(bit)というものの集合体で構成されています。
1ビットで表現できる情報は0/1の2値であり、このビットが8個集まると1バイトとなります。ビット番号は右側ほど小さく表現し、0から始まることに注意が必要です。
1Byteで表現できる数値パターンは256種類ですが、これは28(2の8乗)で表現されるからです。
ビットでの表現は2進数による数値表現そのものであるため、ビットを制御する時は2進数で考えることが基本になります。
ビットを1にすることを「ビットを立てる」、0にすることを「ビットを落とす」と表現します。
開発者の会話では普通に使われるので覚えておきましょう。
signed型変数の最上位ビットの意味
負値を格納できるsigned型の変数において、最上位のビットは特別な意味を持つフラグとなっています。その特別な意味とは「最上位ビットが1の場合、変数は負値である」ということです。
例として10進数における「100」と「-100」を2進数で表現してみます。
このように符号付き変数の場合、最上位ビットは「符号ビット」としての役割を持っています。
この知識は思わぬところで役に立つことがあるため覚えておきましょう。
ビット単位でのデータ管理手法
実際の開発の中ではバイト単位で情報を管理するだけでなく、ビット単位で情報を管理することもあります。
例として学校での学生を管理する情報を1Byteに詰め込んだ情報を定義したとしましょう。
このビット表現において①②③の各ビットを、次のようなルールで管理することがあります。
もしも、各情報を1バイトデータとして管理すると①②③で合計3バイトの領域が必要です。
それに対し、ビット単位に割り付けた場合は1Byteに収めることができますね。膨大なデータを扱う際はこのような手法を用いることでデータ量を圧縮する工夫を行うことがあります。
このようなビット単位の情報をプログラム内で制御するためには、「ビット演算」と呼ばれる処理を用いて読み書きを行うことになります。
ビット演算の種類と基本原理
早く傀儡の術を扱う修行に入りましょうよ。傀儡人形を思い通りに操る感覚を身につけたいんです!この忍術を扱うための極意を習得したいんです!
傀儡人形じゃなくてビットを操るんだけどね。この忍術を扱うためには基本原理を押さえる必要があるよ。基本原理を知った上で実戦で初めて戦えるんだよ。
ビット制御は組み込み開発の中ではよく使われるテクニックです。
慣れないうちは戸惑う方も多いですが、よく使うパターンは決まっているので慣れてしまえばたいしたことはありません。
ビット演算の種類
C言語にはビットを操作するためのビット演算子が用意されています。
ビット演算子を利用することで、任意のビット番号のビットに対して、立てる/落とす/反転する/横に移動するなど様々な制御が可能になります。
ビット演算子には次のものがあります。
論理演算の基礎原理
ビット演算の考えは論理演算と呼ばれるものがベースとなり構成されています。論理演算の基礎原理を知りましょう。
代表的な論理演算である「論理和(OR)」、「論理積(AND)」、「排他的論理和(XOR)」は次の図で表現されます。
AとBの2つの入力に対してYという出力がどうなるかを示したものです。
例えば、論理和であるOR演算では、AもしくはBのどちらかが1の場合は出力Yが1となります。
この法則をビットというものに置き換えて、プログラムで活用します。
ビット演算を使うための捉え方の視点
論理演算の基礎原理で解説した図ですが、組み込み開発者はこの図を少し違う視点で見ています。
この視点の違いにより論理演算は初めてプログラムで活用できる姿になります。
この視点の違いによる操作イメージこそが、本当の意味でビット演算を使いこなす考え方の鍵となります。「BにAを作用させ目的のYを得る」これを頭に叩き込んでください。
ビット演算によるビット制御方法をビット操作の魔術師が教える!
師匠!そろそろ具体的な傀儡の扱い方を教えてください!
ビットという名の傀儡人形を私は操りまくりたいんです。私のチャクラで思い通りに操って見せますよ。
忍者娘、ここからは臨時講師のわしが教えてやる。 これをマスターすれば、お前のような奴でも、思い通りにビットを操ることができるようになるじゃろ。
ビット操作の魔術師が登壇したから僕は帰るね。
プログラムから行うビット演算は次のような流れで利用します。
ビット演算:論理和(OR演算)
論理和である「OR演算子」は、特定のビットを立てる時に利用します。
numという変数に対して1番、4番、7番のビットを立たせたいと思ったら「0x92」を作用させれば実現することができます。
OR演算は作用させるビット値に「1」を指定すると、対象変数の同一ビット部分を強制的に立てた値を作り出すことができます。
着目してほしいのは「0」を指定した部分のビットはnum変数のビット値がそのままキープされていることです。
OR演算を行うことで目的のビットのみを「1」に書き換えることができるのです。
この演算ではnum変数の中身がどのような値であろうと、指定した3つのビットが立つことになります。
OR演算はビットという名の傀儡人形を立たせたい時に使え。8体の傀儡人形に対して立たせたい場所の作用値を「1」にすれば立つじゃろ。
ビット演算:論理積(AND演算)
論理積であるAND演算子はビットを落とす時に利用します。OR演算とは逆の用途ですね。
numという変数に対して1番、4番、7番のビットを落としたいと思ったら「0x6D」を作用させれば実現することができます。
AND演算は作用させる値に「0」を指定すると、対象変数の同一ビット部分を強制的に落とした値を作り出すことができます。
「1」を指定した部分のビットは、num変数のビット値がキープされています。AND演算を行うことで目的のビットのみを「0」に書き換えることができるのです。
「作用させる値」はOR演算の時と指定方法が逆であり、間違いやすいので注意が必要です。
AND演算はビットという名の傀儡人形を座らせたい時に使え。8体の傀儡人形に対して座らせたい場所の作用値を「0」にすれば座るじゃろ。
ビット演算:マスク処理(AND演算のもうひとつの使い方)
AND演算の役割りとしてビットを落とす以外にもう一つの使い方があります。
それがマスク処理です。このマスク処理は他のビット演算と組み合わせて使うことも多い演算です。
マスクとは何か?に関してですが、風邪をひいたときに使うマスクではなく、塗装用マスキングテープのマスクです。
マスク処理を行うと特定のビットの値を抽出することができます。
次のような学生情報が100個存在した場合を想定しましょう。
このデータの中からクラスCの学生を全て抽出したい要件があったとします。プログラムでの判定方法を考えると、クラス情報を示す上位2ビットを取得して「10b」という値になっているかを判定できればよいのです。
このような特定のビット部分を抜き取りたい時に行うのがマスク処理です。
作用させる値として抽出したいビット部分を「1」に設定します。
このようにすると抽出部分のビットイメージのみを残し、それ以外のビットを0クリアすることができます。これがマスクと呼ばれるビット抽出処理です。
マスク処理を扱うときの、ものすごく大事な注意事項
マスク処理をする際にはif文による判定処理がセットで使われることがほとんどです。注意してほしいのは&演算子と比較演算子では比較演算子の方が演算優先度が高いことです。
間違った使い方と正しい使い方のサンプルプログラムを明示します。
// 間違ったマスク判定処理
if (student & 0xC0 == 0x80)
{
printf("クラスCの生徒");
}
// 正しいマスク判定処理
if ((student & 0xC0) == 0x80)
{
printf("クラスCの生徒");
}
このように「&演算子」と「比較演算子」を併用する場合は、必ず括弧による演算優先順位の変更をしてください。
この不具合は皆さん結構やらかします。私は何度も見てきました。
マスク処理は実践でも使われるビット操作テクニックじゃ。マスク処理によるビット抽出方法も身に付けとらんようでは話にならんぞ。
ビット演算:排他的論理和(XOR)
排他的論理和のルールって少し捉えづらいですよね。この演算子は実は捉え方にコツがあります。それは「ビット反転演算子」と覚えることです。
この演算子を作用させると特定のビットの値を反転(0⇔1)することができます。
num変数の1番、2番、4番、6番のビット内容を現在の値から反転したいと思ったら、「0x56」を作用させることで実現できます。
XOR演算は作用させる値に「1」を指定すると、対象となる変数の同一ビット部分の値を反転した値を作り出すことができます。
「0」を指定したビット部分は、num変数のビット値がキープされています。この特性から同じXORを2回実施すると元のデータに戻ることになります。
ビットの値を反転させたいケースは稀じゃがの、使い方なんぞお主たちのアイデア次第じゃ。ここぞという場面が突然現れるかもしれんな。
ビット演算:反転(NOT)
反転は変数の全てのビット値を反転した値を作り出すことができます。
NOT演算子はこれまでの演算子と異なり作用させる値が存在しません。それは、全てのビットを反転させるため指定する必要がないためです。
右シフト(>>)・左シフト(<<)の考え方と使い方
シフト演算はビットの値を左右に移動したい場合に使います。論理シフトと算術シフトと呼ばれるものがあり取扱いに注意が必要です。
ビットシフトには次の法則があります。
- 1ビットの左シフトは変数の値を2倍にする
- 1ビットの右シフトは変数の値を1/2倍にすること
つまり2ビット左シフトすることは2倍×2倍で4倍の値にすることを意味します。
論理シフトの特徴
unsigned型の符号なし変数に対するシフト演算は論理シフトと呼ばれます。このシフトの特徴は範囲外からのビットは0で埋められることです。
算術シフトの特徴
signed型の符号あり変数に対するシフト演算は算術シフトと呼ばれます。符号あり変数の最上位ビットは符号ビットですが、符号ビットの値が1になっている場合、特殊な動きをするため注意が必要です。
右シフトの場合は、符号ビットの値が補填されることに注意が必要です。本例のように符号ビットが1の場合補填されるビットの値は1となります。
符号付き変数の場合で右シフトを使う時は、符号ビットの値が引きずられる可能性を考慮するんじゃ。これを忘れると目的の制御ができん場合があるぞ。要注意じゃ。
Q&A:ビット演算に関するよくある質問
今日はわしがなんでも答えてやる。好きなことを聞け。質問が終わったら、わしはもう帰るぞ。
Q:シフト演算にはなぜ算術シフトなんてややこしい機能があるの?
魔術師先生!私のチャクラが傀儡人形を操りまくってます!この忍術、完全に我がものとなってますよっ。ただ、算術シフトという術がなぜあるのかがわかりません。
忍者娘、なかなか筋がいいぞ。その調子じゃ。「算術シフト」は数値計算上で整合を取るために必要なものなんじゃ。
シフト演算には左にシフトしたときに2倍、右にシフトしたときに1/2倍にするルールが存在しましたね。
算術シフトがある理由は、負の値に対してシフト演算をした時に符号が変化してしまうためです。
例えば、-10を1ビットだけ右シフトすると-5になるべきであり、マイナスの符号は変わっていませんよね。
つまり、符号ビットの状態をキープしないといけない必要があるのです。このようにシフトした結果で符号は変化してはいけないため算術シフトが存在するのです。
Q:ビット演算は実際の開発でもよく使うの?
魔術師先生!この傀儡の術はどんな場面で役に立つんですか、せっかく覚えた術なので使いどころを知りたいんです!
そうじゃな、術も使わねば宝の持ち腐れじゃな。ビット演算は特に組み込み開発ではよく使うんじゃ。ロボットを動かす時には大活躍じゃな。
ビット演算は、組み込み開発のハードウェア制御でよく利用されます。ロボットを動かす際には本当によく出てきますね。
だからこそ、ビット演算の捉え方をしっかりと学んでおくとよいでしょう。
課題:ビット演算が学べたかを確認しよう
課題1
課題内容
次の関数を作成せよ。
printf関数自体には2進数の表示をサポートしていない。printf関数を上手に使い2進数表示を行う必要がある。
main関数から上記関数を呼び出し、出力期待結果の通りに表示されることを確認せよ。
出力期待結果
0x4Cを指定したとき
01001100b
15を指定したとき
00001111b
0xFFを指定したとき
11111111b
main.c
#include <stdio.h>
void printBin(unsigned char bin)
{
int i;
// 上位ビットから順に表示
for (i=7 ; i >= 0 ; i--)
{
// シフトとマスクを利用
printf("%d", (bin >> i) & 0x01);
}
printf("b\n");
}
int main(void)
{
unsigned char num = 0x4C;
// unsigned char num = 15;
// unsigned char num = 0xFF;
// 2進数表示要求
printBin(num);
return 0;
}
この問題はビットというものをパズル的に操るイメージがないとなかなか解けない問題です。右シフト演算とマスク処理を組み合わせることで、目的のビットを抽出しながら上位ビットから順に0/1の出力を行っています。
この問題がスッと解けた人は、ビット演算の考え方をしっかり捉えることができていると思ってよいです。
課題2
課題内容
次の変数を定義せよ。
main関数でbin変数に対して次のオレンジ色部分のビットを立てた値を算出し、画面に2進数で表示せよ。2進数への表示は課題1で作成したprintBin関数を利用せよ。
出力期待結果
binの値が0xA2(10100010b)のとき
11110110b
binの値が0x08(00001000b)のとき
01011100b
main.c
#include <stdio.h>
int main(void)
{
// 10100010b
unsigned char bin = 0xA2;
// 00001000b
// unsigned char bin = 0x08;
// OR演算でビットを立てる
printBin(bin | 0x54);
return 0;
}
0x54をOR演算で作用させる。printBin関数は課題1を流用すること。
OR演算に対する理解を確認する課題ですね。目的のビットを立てたいため、「0x54」を作用させるのが正解です。
2進数のディスプレイ表記は課題1のprintBin関数を流用してください。
課題3
課題内容
次の変数を定義せよ。
main関数でbin変数に対して次のオレンジ色部分のビットを落とした値を算出し、画面に2進数で表示せよ。2進数への表示は課題1で作成したprintBin関数を利用せよ。
出力期待結果
binの値が0x52(01010010b)のとき
00010000b
binの値が0xED(11101101b)のとき
00101100b
main.c
#include <stdio.h>
int main(void)
{
// 01010010b
unsigned char bin = 0x52;
// 11101101b
// unsigned char bin = 0xED;
// AND演算でビットを落とす
printBin(bin & 0x3C);
return 0;
}
AND演算を理解しているかの課題ですね。ビットを落としたい部分に対して作用させる値を「0」で指定することに注意ですね。
つまり「0x3C」を作用させるのが正解です。
課題4
課題内容
次の変数を定義せよ。
main関数でbin変数に対して右と左に2ビットシフトした値を算出し、画面に2進数で表示せよ。
出力期待結果
binの値が0x99(10011001b)のとき
00100110b
01100100b
main.c
#include <stdio.h>
int main(void)
{
// 10011001b
unsigned char bin = 0x99;
// 右シフト
printBin(bin >> 2);
// 左シフト
printBin(bin << 2);
return 0;
}
論理シフトの実施。
unsignedによる論理シフトの課題ですね。シフトさせるとビットがどうなるかをしっかりと確認してください。
課題5
課題内容
次の変数を定義せよ。char型のため注意。
main関数でbin変数に対して右と左に2ビットシフトした値を算出し、画面に2進数で表示せよ。
出力期待結果
binの値が0x99(10011001b)のとき
11100110b
01100100b
main.c
#include <stdio.h>
int main(void)
{
// 10011001b
char bin = 0x99;
// 右シフト
printBin(bin >> 2);
// 左シフト
printBin(bin << 2);
return 0;
}
signedによる符号付き変数の算術シフトの課題です。右シフトする場合は符号ビットが引きずられることに注目しましょう。
課題6
課題内容
学生IDとして1Byteを次のビットで表現しているものとする。
学生IDを生成する次の関数を作成せよ。
main関数において上記関数を呼び出し、生成した学生IDを2進数で画面に表示せよ。
出力期待結果
makeStudentID関数へcls=2、no=30、gen=1を渡したとき
10111101b
makeStudentID関数へcls=3、no=11、gen=0を渡したとき
11010110b
main.c
#include <stdio.h>
unsigned char makeStudentID(unsigned char cls, unsigned char no, unsigned char gen)
{
// AND演算、OR演算、シフト合わせる
return (cls & 0x03) << 6 | (no & 0x1F) << 1 | (gen & 0x01);
}
int main(void)
{
unsigned char student;
student = makeStudentID(2, 30, 1);
// student = makeStudentID(3, 11, 0);
// 学生情報表示
printBin(student);
return 0;
}
実践的なビット演算の複合課題です。ビット演算は複数組み合わせることも多いので、各演算がどのような作用をもたらすかを正確に把握しておく必要があります。
この問題は、複数の情報を1つの情報へ統合するときのパターンです。
課題7
課題内容
次の関数を作成せよ。学生IDは課題6と同様とする。
main関数において上記関数を呼び出し、学生情報を画面に表示せよ。
出力期待結果
studentIDが0xBDのとき
クラス:2
出席番号:30
性別:1
studentIDが0xD6のとき
クラス:3
出席番号:11
性別:0
main.c
#include <stdio.h>
void printStudentID(unsigned char student)
{
unsigned char cls;
unsigned char no;
unsigned char gen;
// シフトとANDでデータ取得
cls = (student >> 6) & 0x03;
no = (student >> 1) & 0x1F;
gen = student & 0x01;
// 各データを表示
printf("クラス:%d\n", cls);
printf("出席番号:%d\n", no);
printf("性別:%d\n", gen);
return;
}
int main(void)
{
printStudentID(0xBD);
// printStudentID(0xD6);
return 0;
}
マスク処理を利用し目的のデータを抽出する。
この問題も複合パターンですね。こちらはデータをビット単位で抽出して分離するケースになります。これもよく使われるテクニックですね。