C言語 ビット演算【扱うための視点と実践的な使用例を図解】

C言語
この記事は約19分で読めます。

こんにちは、ナナです。

2進数と16進数を学んだところでビット演算と呼ばれる演算方法を学んでみましょう。

進数について詳しく知りたい方は『C言語 2進数 16進数 考え方と変換方法【0と1で数を表現?】』の記事を参照してください。

ビット演算の方法が書かれたサイトはありますが、どういう場面でどのように使うのかという実践的なシーンに言及しているものは少ないです。

本記事では次の疑問点を解消する内容となっています。

本記事で学習できること
  • ビットとは何か?ビットを使った情報管理とは?
  • ビット演算の基礎となる論理演算とは?
  • ビット演算を使いこなすために知るべき視点とイメージとは?
  • ビット演算を使ったビット制御方法と特徴とは?
  • OR演算、AND演算、XOR演算、NOT演算、シフト演算の使い方とは?

では、ビット演算の使い方を学んでいきましょう。

スポンサーリンク

ビット演算を学ぶ前にビットを知ろう

師匠!数値情報を操るための「ビット演算」と呼ばれる傀儡くぐつの術があると聞いたことがあります。そろそろ、私のチャクラ量もごりごり上がってきていますし、扱える時期に来たんじゃないですか?教えてくださいっ!

ナナ
ナナ

「ビット演算」だね。そろそろ扱ってもいい頃かもね。じゃあ、ビット演算を知る前にビットとは何なのかから学んでいこう!

ビットの概念

メモリというハードウェアは1バイトという単位で存在し、順番に記憶領域が並んでいます。1バイトとはビット(bit)というものの集合体で構成されています。

1ビットで表現できる情報は0/1の2値であり、このビットが8個集まると1バイトとなります。ビット番号は右側ほど小さく表現し、0から始まることに注意が必要です。

bit構成

1Byteで表現できる数値パターンは256種類ですが、これは28(2の8乗)で表現されるからです。

ビットでの表現は2進数による数値表現そのものであるため、ビットを制御する時は2進数で考えることが基本になります。

ナナ
ナナ

ビットを1にすることを「ビットを立てる」、0にすることを「ビットを落とす」と表現します。

開発者の会話では普通に使われるので覚えておきましょう。

signed型変数の最上位ビットの意味

負値を格納できるsigned型の変数において、最上位のビットは特別な意味を持つフラグとなっています。その特別な意味とは「最上位ビットが1の場合、変数は負値である」ということです。

例として10進数における「100」と「-100」を2進数で表現してみます。

符号フラグ

このように符号付き変数の場合、最上位ビットは「符号ビット」としての役割を持っています。
この知識は思わぬところで役に立つことがあるため覚えておきましょう。

ビット単位でのデータ管理手法

実際の開発の中ではバイト単位で情報を管理するだけでなく、ビット単位で情報を管理することもあります。

例として学校での学生を管理する情報を1Byteに詰め込んだ情報を定義したとしましょう。

BIT単位のデータ管理

このビット表現において①②③の各ビットを、次のようなルールで管理することがあります。

学生情報

もしも、各情報を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演算

OR演算は作用させるビット値に「1」を指定すると、対象変数の同一ビット部分を強制的に立てた値を作り出すことができます。

着目してほしいのは「0」を指定した部分のビットはnum変数のビット値がそのままキープされていることです。

OR演算を行うことで目的のビットのみを「1」に書き換えることができるのです。

この演算ではnum変数の中身がどのような値であろうと、指定した3つのビットが立つことになります。

魔術師
魔術師

OR演算はビットという名の傀儡人形を立たせたい時に使え。8体の傀儡人形に対して立たせたい場所の作用値を「1」にすれば立つじゃろ。

ビット演算:論理積(AND演算)

論理積であるAND演算子はビットを落とす時に利用します。OR演算とは逆の用途ですね。

numという変数に対して1番、4番、7番のビットを落としたいと思ったら「0x6D」を作用させれば実現することができます。

AND演算

AND演算は作用させる値に「0」を指定すると、対象変数の同一ビット部分を強制的に落とした値を作り出すことができます。

「1」を指定した部分のビットは、num変数のビット値がキープされています。AND演算を行うことで目的のビットのみを「0」に書き換えることができるのです。

「作用させる値」はOR演算の時と指定方法が逆であり、間違いやすいので注意が必要です。

魔術師
魔術師

AND演算はビットという名の傀儡人形を座らせたい時に使え。8体の傀儡人形に対して座らせたい場所の作用値を「0」にすれば座るじゃろ。

ビット演算:マスク処理(AND演算のもうひとつの使い方)

AND演算の役割りとしてビットを落とす以外にもう一つの使い方があります。

それがマスク処理です。このマスク処理は他のビット演算と組み合わせて使うことも多い演算です。

マスクとは何か?に関してですが、風邪をひいたときに使うマスクではなく、塗装用マスキングテープのマスクです。

マスク処理を行うと特定のビットの値を抽出することができます。

マスク

次のような学生情報が100個存在した場合を想定しましょう。

このデータの中からクラスCの学生を全て抽出したい要件があったとします。プログラムでの判定方法を考えると、クラス情報を示す上位2ビットを取得して「10b」という値になっているかを判定できればよいのです。

クラス情報

このような特定のビット部分を抜き取りたい時に行うのがマスク処理です。

マスク処理

作用させる値として抽出したいビット部分を「1」に設定します。

このようにすると抽出部分のビットイメージのみを残し、それ以外のビットを0クリアすることができます。これがマスクと呼ばれるビット抽出処理です。

マスク処理を扱うときの、ものすごく大事な注意事項

マスク処理をする際にはif文による判定処理がセットで使われることがほとんどです。注意してほしいのは&演算子と比較演算子では比較演算子の方が演算優先度が高いことです。

間違った使い方と正しい使い方のサンプルプログラムを明示します。

// 間違ったマスク判定処理
if (student & 0xC0 == 0xC0)
{
    printf("クラスCの生徒");
}

// 正しいマスク判定処理
if ((student & 0xC0) == 0xC0)
{
    printf("クラスCの生徒");
}

このように「&演算子」と「比較演算子」を併用する場合は、必ず括弧による演算優先順位の変更をしてください。

この不具合は皆さん結構やらかします。私は何度も見てきました。

魔術師
魔術師

マスク処理は実践でも使われるビット操作テクニックじゃ。マスク処理によるビット抽出方法も身に付けとらんようでは話にならんぞ。

ビット演算:排他的論理和(XOR)

排他的論理和のルールって少し捉えづらいですよね。この演算子は実は捉え方にコツがあります。それは「ビット反転演算子」と覚えることです。

この演算子を作用させると特定のビットの値を反転(0⇔1)することができます。

num変数の1番、2番、4番、6番のビット内容を現在の値から反転したいと思ったら、「0x56」を作用させることで実現できます。

XOR演算

XOR演算は作用させる値に「1」を指定すると、対象となる変数の同一ビット部分の値を反転した値を作り出すことができます。

「0」を指定したビット部分は、num変数のビット値がキープされています。この特性から同じXORを2回実施すると元のデータに戻ることになります。

魔術師
魔術師

ビットの値を反転させたいケースは稀じゃがの、使い方なんぞお主たちのアイデア次第じゃ。ここぞという場面が突然現れるかもしれんな。

ビット演算:反転(NOT)

反転は変数の全てのビット値を反転した値を作り出すことができます。

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進数表示を行う必要がある。

課題1_1

main関数から上記関数を呼び出し、出力期待結果の通りに表示されることを確認せよ。


出力期待結果

0x4Cを指定したとき

01001100b

15を指定したとき

00001111b

0xFFを指定したとき

00001111b

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

課題内容

次の変数を定義せよ。

課題2_1

main関数でbin変数に対して次のオレンジ色部分のビットを立てた値を算出し、画面に2進数で表示せよ。2進数への表示は課題1で作成したprintBin関数を利用せよ。

課題2_2

出力期待結果

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

課題内容

次の変数を定義せよ。

課題3_1

main関数でbin変数に対して次のオレンジ色部分のビットを落とした値を算出し、画面に2進数で表示せよ。2進数への表示は課題1で作成したprintBin関数を利用せよ。

課題3_2

出力期待結果

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

課題内容

次の変数を定義せよ。

課題4_1

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型のため注意。

課題5_1

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を次のビットで表現しているものとする。

BIT単位のデータ管理
学生情報

学生IDを生成する次の関数を作成せよ。

課題6_1

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と同様とする。

課題7_1

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;
}

マスク処理を利用し目的のデータを抽出する。

ナナ
ナナ

この問題も複合パターンですね。こちらはデータをビット単位で抽出して分離するケースになります。これもよく使われるテクニックですね。