C言語のビット演算(論理演算)について解説

ビット演算解説ページのアイキャッチ

このページにはプロモーションが含まれています

基本的にC言語で扱うデータはバイト単位です。ですので、ビット単位で演算を行うビット演算(論理演算)自体使ったことがないという方も多いのではないかと思います。

ただビット単位でデータを管理したりビット演算を行うことでメリットもありますので、使いこなせるとより性能の高いプログラムを作ることもできます。

このページではそのビット演算とC言語での使い方について解説します。

ビット演算とは

まずはビット演算がどのようなものであるかを、ビットとバイトの知識を踏まえて簡単に解説します。

ビットとは

ビットとはコンピュータが扱うデータ量の最小単位です。1ビットが表すことができるのは2つの状態のみで、主に “0” と “1” を表すことに用いられます。

ビットの説明図

スポンサーリンク

バイトとは

バイトもデータ量の単位です。具体的には、ビットが8個集まったデータ量の単位であり、ビット1個で2つの状態を表すことができますので、1バイトでビットが8個で256の状態(2の8乗)を表現することができます。

バイトの説明図

このようなビットの列に対し、各ビットを区別して呼ぶために、各ビットは一番右のビットからの距離Xを用いて第 Xビットと呼びます。一番右のビットは第0ビット、一つ左隣は第1ビット、そのさらに左隣は第2ビット…、といった感じですね。

各ビットの呼び方

C言語であれば char 型や unsigned char 型がサイズが1バイトの型であり、それぞれ数字だと -128 〜 127、0 〜 255 の値を扱うことができ、256個の状態を表現可能であることが分かると思います。

short 型や int 型などは一般的に型のサイズはそれぞれ2バイトや4バイトであり、1バイトよりもさらに多くの状態を表現することが可能です。ただ、これらは結局全てビットという最小単位が集まって構成されているデータとなります。

大きいサイズのバイトのビットの集まり

ビット演算とは

そして、ビット演算とはビット単位で演算を行う処理のことを言います。基本的にプログラムで扱うのはバイト単位のデータで、ビットをあまり意識することはありません。

ビット演算ではそのビットを意識し、バイトデータの中の特定のビットの値を変更する・特定のビットの値を取得する等の演算を行います。

ビット演算のイメージ

ビット単位で演算を行うことができれば、バイト単位ではなくビット単位でデータを管理することができるようになります。

ビット演算のメリット

ではそのビット演算を行うことでどのようなメリットがあるでしょうか?ここについて解説していきたいと思います。

スポンサーリンク

使用メモリの削減

まずメリットの一つはプログラム中で使用するメモリを削減することが可能であることです。

具体例を示してこの効果を説明したいと思います。

例えば社員の資格試験合格状況を管理するデータ構造を考えてみましょう。ここでは下記の8つの資格試験合格状況を管理するとして説明します。

  • FE(基本情報技術者試験)
  • AP(応用情報技術者試験)
  • ST(ITストラテジスト試験)
  • SA(システムアーキテクト試験)
  • PM(プロジェクトマネージャ試験)
  • NW(ネットワークスペシャリスト試験)
  • DB(データベーススペシャリスト試験)
  • ES(エンベデッドシステムスペシャリスト試験)

このデータを管理するデータ構造としてどのようなものが思い浮かぶでしょうか?一番最初に思いつくのは下記のような構造体なのではないかと思います。

struct pass_info {
  char hasFE;
  char hasAP;
  char hasST;
  char hasSA;
  char hasPM;
  char hasNW;
  char hasDB;
  char hasES;
};

構造体の各メンバに合格している場合は “1” 、合格していない場合は “0” を格納するようにすれば各資格試験の合格状況を管理することができますね。

各資格試験合格状況は、下記のようにメンバの値を取得すれば確認することも可能です(下記は DB に合格しているかを判断している if 文)。

struct pass_info taro;

/* 略 */

if(taro.hasDB == 1){
  /* taro が DB に合格している場合の処理 */
} else {
  /* taro が DB に合格していない場合の処理 */
}

各メンバは char 型ですので一人分を管理するのに8バイト(1バイト x 8)のメモリが必要になります。

一方で、ビット演算が使えれば、下のようにビット単位で資格試験の合格状況を管理することができます。

  • 第0ビット:FE の合格状況
  • 第1ビット:AP の合格状況
  • 第2ビット:ST の合格状況
  • 第3ビット:SA の合格状況
  • 第4ビット:PM の合格状況
  • 第5ビット:NW の合格状況
  • 第6ビット:DB の合格状況
  • 第7ビット:ES の合格状況

つまり、バイトの中の各々のビットに対して各資格試験の合格状況を “1” と “0” で表現するというわけです。

構造体で管理する場合は一人あたり8バイトのメモリが必要ですが、この方法であれば一人あたり1バイトのメモリしか必要ありませんので7バイトのメモリを節約できているということになります。10000人であれば70000バイト(約70KB)の節約です。

構造体の時のように DB 試験に合格しているかどうかを判断して処理するソースコードは下記のようになります。

unsigned char taro;

/* 略 */

if((taro & 64) >> 6 == 1){
  /* taro が DB に合格している場合の処理 */
} else {
  /* taro が DB に合格していない場合の処理 */
}

ここで使っている “&” と “>>” がビット演算子ですね(特に “>>” はシフト演算子とも呼ばれます)。これらがどういう動きをするかは基本的なビット演算で解説します。

この例からも分かるように、ビット単位で情報を管理し、それをビット演算で操作することができれば、使用メモリの削減をすることが可能です。

プログラムの高速化

もう一つのメリットとしてプログラムの処理速度を高速化可能であることが挙げられます。

こちらも例を挙げて解説したいと思います。次のプログラムは0から10億までの整数が「8の倍数であるかどうか」をチェックするものになります。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define N 1000000000

int main(void){
    long long i;
    char *r;
    clock_t start, end;
    FILE *fo;

    r = (char*)malloc(sizeof(char) * N);

    start = clock();
    for(i = 0; i <= N; i++){
        if((r[i] % 8) == 0){
            r[i] = 1;
        } else { 
            r[i] = 0;
        } 
    }
    end = clock();

    printf("processing time:%.3f[sec]\n",
      ((double)end - (double)start) / CLOCKS_PER_SEC);
    
    free(r);
    return 0;
}

最適化なしでコンパイルして実行すると、「約6.4秒」の時間がかかりました。

ビット演算を用いた場合、下記部分は

if((r[i] % 8) == 0){

下記のように変更することで各数字ば「8の倍数であるかどうか」を判断することができます(なぜこれで「8の倍数であるかどうか」を判断できるかは後述します)。

if((r[i] & 7) == 0){

変更後のソースコードを最適化なしでコンパイルして実行すると「約4.1秒」の時間で処理が完了し、高速化を行うことができています。

コンピュータは基本的にビット演算が得意で処理が速いんですよね。ですのでビット演算を用いることでプログラムの高速化することが可能です。

MEMO

ちなみに高速化の最適化オプションをつけてコンパイルすると同じくらいの処理時間になりました。が、高速化の最適化オプションが常に付加できるとは限りませんので、特にこのオプションが付けられない時にビット演算が有効です。

基本的なビット演算

それではここからは、ここまで解説してきたビット演算をC言語で実際にどのように行うかを解説していきたいと思います。

ビット演算で行う基本的な演算は「AND 演算」「OR 演算」「NOT 演算」「XOR演算」「左シフト演算」「右シフト演算」の6つになります。

スポンサーリンク

AND 演算

AND 演算とは2つの入力ビットに対し、「両方のビットが “1” の場合のみ “1” に、それ以外の場合は “0” にする演算」です。 

入力ビット1 入力ビット2 出力ビット
0 0 0
0 1 0
1 0 0
1 1 1

C言語ではこの AND 演算を “&” 演算子を用いて実行することができます。

byteC = byteA & byteB;

これにより、2つの整数の各ビットに対して AND 演算が実行されます。

AND演算の説明図

例えば下記のようなプログラムの場合、byteA と byteB 両方でビットが “1” になっている第6ビットのみ “1” で、他のビットが “0” の値が byteC に格納されることになります。

#include <stdio.h>
  
void printBin(char);

/* 入力値を2進数で表示する関数 */
void printBin(char x){
  char bitNum;
  char value;
  char i;

  /* 入力値の型のサイズのビット数を bitNum に格納 */
  /* 入力値をchar型にしているので必ず8になる */
  bitNum = sizeof(char) * 8;

  printf("0b");
  for(i = bitNum - 1; i >= 0; i--){
    /* 第iビットの値を取得 */
    value = (x & (1 << i)) >> i;

    /* 取得した値を表示 */
    printf("%d", value);
  }
  printf("\n");
  
}

int main(void){

  char byteA;
  char byteB;
  char byteC;

  byteA = 0b11000110;
  byteB = 0b01011001;

  byteC = byteA & byteB;

  printBin(byteC);

  return 0;
}

表示結果は下記のようになります。

0b01000000
MEMO①

0b は二進数表記を明示的に表すための識別子です。定数の前にこれを付加することで、その定数を二進数として値を格納したり計算に用いたりすることができます。

MEMO②

printf 関数では引数の値を二進数表記で表示することはできません(16進数ならフォーマット指定子に “%x” を用いれば表示可能だが、二進数向けの指定子はない)。

ですので上のプログラムでは二進数表記で表示するための printBin 関数を自作しています。今後もこの printBin 関数は使用しますが、ソースコードから printBin 関数の定義部分を含めた main 関数以外は省略しますのでご了承ください(#include <stdio.h> も省略してます)。

OR 演算

OR 演算とは2つの入力ビットに対し、「両方のビットが “0” の場合のみ “0” に、それ以外の場合は “1” にする演算」です。 つまり片方のビットでも “1” であれば結果も “1” になります。

入力ビット1 入力ビット2 出力ビット
0 0 0
0 1 1
1 0 1
1 1 1

C言語ではこの OR 演算を “|” 演算子を用いて実行することができます。

byteC = byteA | byteB;

これにより、2つの整数の各ビットに対して OR 演算が実行されます。

OR演算の説明図

例えば下記のようなプログラムの場合、byteA と byteB で片方のビットでも “1” になっている第5ビット以外のビットが “1” で、第5ビットのみ “0” の値が byteC に格納されることになります。

int main(void){

  char byteA;
  char byteB;
  char byteC;

  byteA = 0b11000110;
  byteB = 0b01011001;

  byteC = byteA | byteB;

  printBin(byteC);

  return 0;
}

表示結果は下記のようになります。

0b11011111

NOT 演算

NOT 演算とは1つの入力ビットに対し、「そのビットの値が “0” の場合 “1” に、”1″ の場合 “0” にする演算」です。つまり入力値を反転させる演算です。

入力ビット 出力ビット
0 1
1 0

C言語ではこの NOT 演算を “~” 演算子を用いて実行することができます。

byteB = ~byteA;

これにより、1つの整数の各ビットに対して NOT 演算が実行されます。

NOT演算の説明図

例えば下記のようなプログラムの場合、byteA の各ビットが反転された値が byteB に格納されることになります。

int main(void){

  char byteA;
  char byteB;

  byteA = 0b11000110;

  byteB = ~byteA;

  printBin(byteB);

  return 0;
}

表示結果は下記のようになります。

0b00111001

スポンサーリンク

XOR 演算

XOR 演算とは2つの入力ビットに対し、「片方のビットが “1” の場合のみ “1” に、それ以外の場合は “0” にする演算」です。 つまり両方のビットが同じであれば “0” に、異なる場合は “1” になります。

入力ビット1 入力ビット2 出力ビット
0 0 0
0 1 1
1 0 1
1 1 0

C言語ではこの XOR 演算を “^” 演算子を用いて実行することができます。

byteC = byteA ^ byteB;

これにより、2つの整数の各ビットに対して XOR 演算が実行されます。

XOR演算の説明図

例えば下記のようなプログラムの場合、XOR 演算により byteA と byteB で両方が同じ値である第1ビット、第4ビット、第7ビットが “0” になり、その他のビットが “1” になります。

int main(void){

  char byteA;
  char byteB;
  char byteC;

  byteA = 0b10010011;
  byteB = 0b11111110;

  byteC = byteA ^ byteB;
  printBin(byteC);

  return 0;
}

表示結果は下記の通りです。

0b01101101

左シフト演算

左シフト演算とは1つの値の各ビットを「左方向に指定したビット分シフトする(ずらす)演算」です。

C言語ではこの左シフト演算を “<<” 演算子を用いて実行することができます。下記は3ビット分左方向にシフトするシフト演算を行う例です。

byteB = byteA << 3;

左シフト演算では、各ビットをシフトすることで空いてしまったビットには “0” が格納されます。また左方向に溢れてしまったビットは捨てられます。 

左シフト演算の説明図

例えば下記のプログラムでは、byteA と byteB に対して各ビットを左方向に3ビット分シフトすることになります。

int main(void){
  char byteA;
  unsigned char byteB;

  byteA = 0b10010011;
  byteB = 0b11110001;

  printBin(byteA << 3);
  printBin(byteB << 3);
  return 0;
}

表示結果は下記のようになります。

0b10011000
0b00001000

各ビットが左方向に3ビットずつシフトしていること、空いた右側の3つのビットには “0” が格納されていること、溢れてしまった左側の3つのビットは捨てられていることが確認できると思います。

右シフト演算とは1つの値の各ビットを「右方向に指定したビット分シフトする(ずらす)演算」です。

一言で言うと、右シフト演算は左シフトの右バージョンの演算です。ですが、シフトさせる値の型が「符号あり」か「符号なし」かで動作が異なりますのでその点は注意ポイントになります。

C言語ではこの右演算を “>>” 演算子を用いて実行することができます。下記は3ビット分右方向にシフトするシフト演算を行う例です。

byteB = byteA >> 3;

シフトさせる変数の型が「符号なし」の場合、右シフトを行うと各ビットをシフトすることで空いてしまったビットには “0” が格納されます。また右方向に溢れてしまったビットは捨てられます。 

右シフト演算(符号なし)の説明図

一方で、シフトさせる変数の型が「符号あり」の場合、右シフトを行うと各ビットをシフトすることで空いてしまったビットには “最上位ビットの値” が格納されます。

つまり、最上位ビットが “1” であればシフトによって空いてしまった全てのビットに “1” が格納されますし、最上位ビットが “0” であればシフトによって空いてしまった全てのビットに “0” が格納されます。

右シフト演算(符号あり)の説明図

ここが「符号なし」の場合と違う点です。また右方向に溢れてしまったビットは捨てられます(ここは符号なしの場合と同じ)。 

例えば下記のようなプログラムであれば、byteA・byteB・byteC・byteD に対して各ビットを右方向に3ビット分シフトすることになります。

int main(void){

  char byteA;
  unsigned char byteB;
  char byteC;
  unsigned char byteD;

  byteA = 0b10010011;
  byteB = byteA;
  byteC = 0b01110001;
  byteD = byteC;

  printBin(byteA >> 3);
  printBin(byteB >> 3);
  printBin(byteC >> 3);
  printBin(byteD >> 3);

  return 0;
}

表示結果は下記のようになります。

0b11110010
0b00010010
0b00001110
0b00001110

byteA と byteB、byteC と byteD はそれぞれ同じ値が格納されていますが、符号の有無で右シフト演算結果が異なることが確認できると思います。

ちょっとややこしいですが、このように動きが異なるにはそのメリットがあるからです。どのようなメリットがあるかは2のべき乗の掛け算・割り算を行うで説明します。

スポンサーリンク

ビット演算のいろいろな使い方

続いてビット演算でどのようなことができるかを解説していきたいと思います。

具体例がある方が分かりやすいかと思って「特定のビットを “1” にする」「特定のビットを “0” にする」「特定のビットの値を調べる」「特定の複数のビットが全て “1” かどうかを調べる」については使用メモリの削減で挙げた資格試験合格状況管理を例にプログラムを作成しています。

使用メモリの削減のプログラムに目を通しておいていただけるとよりわかりやすくなりますので、是非事前に読んでおいていただくと良いと思います。

データの各ビットに格納されている情報のみ、ここに再掲しておきます。

  • 第0ビット:FE の合格状況
  • 第1ビット:AP の合格状況
  • 第2ビット:ST の合格状況
  • 第3ビット:SA の合格状況
  • 第4ビット:PM の合格状況
  • 第5ビット:NW の合格状況
  • 第6ビット:DB の合格状況
  • 第7ビット:ES の合格状況

特定のビットを “1” にする

特定のビットのみを “1” にするためには OR 演算を用います。

OR は2つの入力ビットに対してどちらか一方でも “1” であれば演算結果は “1” になります。

ですので下記の手順で特定のビットのみを “1” にすることができます。

  • “1” にしたいビットのみを “1” に、それ以外のビットを “0” としたデータ(値)を作成する
  • ビットを “1” にしたいデータと作成したデータの各ビットに対して OR 演算を行う

たとえば「taro が AP と NW に合格したので  AP と NW のみのビットを “1” にする」場合は下記のように記述します。

int main(void){
  char taro;
  char bits;

  /* taroはFEとPMは合格済み */
  taro = 0b00010001;

  /* APとNWのビットを1にするために、
     第1ビットと第5ビットのみを1にした
     データbitsを作成 */
  bits = 0b00100010;

  /* taroとbitsの各ビットに対してOR演算 */
  /* 元々のtaroビット列に対して
     bitsで"1" になっているビットが"1"になる */
  taro = taro | bits;

  printBin(taro);

  return 0;
}

表示結果は下記の通りです。

0b00110011

元々の “00010001” が OR 演算を実行することで第1ビットと第5ビットが “1” に変わっていることが確認できると思います。

特定のビットを “0” にする

特定のビットのみを “0” にするためには AND 演算を用います。

AND は2つの入力ビットに対してどちらか一方でも “0” であれば演算結果は “0” になります。

ですので下記の手順で特定のビットのみを “0” にすることができます。

  • “0” にしたいビットのみを “0” に、それ以外のビットを “1” としたデータ(値)を作成する
  • ビットを “0” にしたいデータと作成したデータの各ビットに対して AND 演算を行う

たとえば「taro の FE と AP の有効期限が切れたて合格が無効になったので、 FE と AP のみのビットを “0” にする」場合は下記のように記述します(おそらく実際の FE や AP に有効期限はないと思いますが…)。

int main(void){
  char taro;
  char bits;

  /* taroはFE,PM,AP,NWに合格済み */
  taro = 0b00110011;

  /* FEとAPのビットを0にするために、
     第0ビットと第1ビットのみを0にした
     データbitsを作成 */
  bits = 0b11111100;

  /* taroとbitsの各ビットに対してAND演算 */
  /* 元々のtaroビット列に対して
     bitsで"0" になっているビットが"0"になる */
  taro = taro & bits;

  printBin(taro);

  return 0;
}

表示結果は下記の通りです。

0b00110000

元々の “00110011” が AND 演算を実行することで第1ビットと第0ビットが “0” に変わっていることが確認できると思います。

スポンサーリンク

特定の1つのビットの値を調べる

まずはビット演算を用いたデータの特定のビットの値(”1″ or “0”)を調べる方法について解説します。これには主に AND 演算を用います。

AND 演算は2つのビット値の両方が “1” のときのみ結果が “1” となる演算です。ですので、下記のように処理することで特定のビットの値を調べることが可能です。

  • 調べたいビットのみを “1” に、その他のビットを “0” に設定したデータ(値)を作成
  • 調べたいデータと作成したデータの各ビットに対して AND 演算を実行
    • AND 演算結果が “0” 以外であれば、調べたいビットの値は “1” と判断
    • AND 演算結果が “0” であれば調べたいビットの値も “0” と判断

たとえば「taro が DB に合格しているかどうかを確認する」場合は下記のように記述します。

int main(void){
  char taro;
  char bits;

  /* taroはFE,PM,AP,NWに合格済み */
  taro = 0b00110011;

  /* 第6ビット(DB)のみを1にしたデータbitsを作成 */
  bits = 0b01000000;

  /* taroとbitsの各ビットに対してAND演算 */
  if((taro & bits) != 0){
    /* 演算結果が0でなければtaroも第6ビットが"1" */
    printf("taroはDBに合格済みです\n");
  } else {
    /* 演算結果が0であればtaroの第6ビットは"0" */
    printf("taroはDBに合格していません\n");
  }

  return 0;
}

表示結果は下記の通りです。

taroはDBに合格していません

特定の複数のビットが全て “1” かどうかを調べる

「特定の1つのビットの値を調べる」の方法の応用で、特定の複数のビットが全て “1” かどうかを調べる事が可能です。このような複数の条件を満たすかどうかは下記のように処理することで調べることができます。

  • 調べたい複数ビットのみを “1” に、その他のビットを “0” に設定したデータ(値)を作成
  • 調べたいデータと作成したデータの各ビットに対して AND 演算を実行
    • AND 演算結果が「作成したデータと同じ値」であれば、調べたいビットの値は “1” と判断
    • AND 演算結果が「作成したデータと違う値」であれば調べたいビットの値も “0” と判断

たとえば「taro が FE と AP と PM に合格しているかどうかを確認する」場合は下記のように記述します。

int main(void){
  char taro;
  char bits;

  /* taroはFE,PM,AP,NWに合格済み */
  taro = 0b00110011;

  /* 第0ビット(FE),第1ビット(AP),第4ビット(PM)
     を1にしたデータbitsを作成 */
  bits = 0b00010011;

  /* taroとbitsの各ビットに対してAND演算 */
  if((taro & bits) ==  bits){
    /* 演算結果がbitsと同じであれば
       taroも第0,第1,第4ビットが全て"1" */
    printf("taroはFE,AP,PM全てに合格済みです\n");
  } else {
    /* 演算結果がbitsと同じでなければ
       taroは第0,第1,第4ビットのどれか1つ以上が"0" */
    printf("taroはFE,AP,PMのどれかに合格していません\n");
  }

  return 0;
}

表示結果は下記の通りです。

taroはFE,AP,PM全てに合格済みです

2進数表示する

16進数や10進数は printf 関数で表示することが可能ですが、C言語では2進数表示する標準関数はありません。しかし2進数表示は「特定の1つのビットの値を調べる」を応用することで簡単に実現することが可能です。

単純に最上位ビットから順に最下位ビットまでビットの値が “1” か “0” かどうかを調べ、”1″ の場合は “1” を、”0″ の場合は “0” を表示していけば良いだけです。

入力値(char型)を2進数で表示する関数は下記の通りです。もっと大きい値を

/* 入力値を2進数で表示する関数 */
void printBin(char x){
  char bitNum;
  char value;
  char i;

  /* 入力値の型のサイズのビット数を bitNum に格納 */
  /* 入力値をchar型にしているので必ず8になる */
  bitNum = sizeof(char) * 8;

  printf("0b");
  /* 最上位ビットから最下位ビットまで
     ビットの値を順に表示 */
  for(i = bitNum - 1; i >= 0; i--){
    /* 第iビットの値を取得 */
    value = (x & (1 << i)) >> i;

    /* 取得した値を表示 */
    printf("%d", value);
  }
  printf("\n");

}

ポイントは下記の部分になります。

value = (x & (1 << i)) >> i;

「1 << i」は “1” (つまり00000001)を i ビット分左シフトする演算ですので、「第iビットのみを “1” にする演算」になります。ですので、その結果と入力値の各ビットで AND 演算を行えば、入力値の第 i ビットはそのままで、それ以外のビットが全て “0” の値を取得することが可能です。

さらにそれを i ビット分右シフトすれば、入力値の第 i ビットの値のみを取得することができますので、それを出力してやれば “1” or “0” の値を表示することができます。

スポンサーリンク

偶数・奇数を判定する

ビット演算を行わない場合は下記のように判定するのが一般的だと思います。

if(a % 2){
    printf("aは偶数です\n");
  } else {
    printf("aは奇数です\n");
  }

偶数・奇数の判定も AND 演算により行うことが可能です。偶数と奇数の数字では第0ビットな値が必ず下記のようになります。

  • 偶数の場合:第0ビットが “0”
  • 奇数の場合:第0ビットが “1”

したがって第0ビットの値を調べてやれば偶数か奇数かを判定することができます。そして前述のとおり、特定のビットの値を調べるのには AND 演算を用いれば良いです。

AND 演算を用いた「偶数・奇数の判定を行う」プログラム例は下記なようになります。

int main(void){
  int a = 31;
  int b = 42;

  /* 偶数奇数判定したい値と"1"のAND演算 */
  /* "1"は8桁2進数で表すと"00000001"なので
     偶数奇数判定したい値の第0ビットの値が取得できる */
  if(a & 1){
    printf("aは奇数です\n");
  } else {
    printf("aは偶数です\n");
  }

  if(b & 1){
    printf("bは奇数です\n");
  } else {
    printf("bは偶数です\n");
  }

  return 0;
}

表示結果は下記の通りです。

aは奇数です
bは偶数です

2のべき乗の掛け算・割り算を行う

2のべき乗の掛け算は左シフト演算、2のべき乗の割り算は右シフト演算でそれぞれ実行可能です。

ある整数をXビット分左シフトすると、「2のX乗」倍した値が得られます。

またある整数をYビット分右シフトすると、「2のX乗」分の1した値が得られます。ちなみに余りが出た場合は切り捨てが行われます。

左シフトと右シフトを用いて2のべき乗の掛け算・割り算を行うプログラム例は下記の通りです。

int main(void){

  char valueA;
  char valueB;
  char valueC;
  char valueD;

  valueA = 5;
  valueB = valueA << 3;
  valueC = -100;
  valueD = valueC >> 3;

  printf("+++ 10進数表示 +++\n");
  printf("valueA = %d\n", valueA);
  printf("valueB = %d\n", valueB);
  printf("valueC = %d\n", valueC);
  printf("valueD = %d\n", valueD);

  printf("+++ 2進数表示 +++\n");
  printf("valueA = ");
  printBin(valueA);
  printf("valueB = ");
  printBin(valueB);
  printf("valueC = ");
  printBin(valueC);
  printf("valueD = ");
  printBin(valueD);

  return 0;
}

表示結果は下記の通りです。

+++ 10進数表示 +++
valueA = 5
valueB = 40
valueC = -100
valueD = -13
+++ 2進数表示 +++
valueA = 0b00000101
valueB = 0b00101000
valueC = 0b10011100
valueD = 0b11110011

まず見ていただきたいのが10進数の表示結果です。3ビットシフトしているので、左シフトした結果は8(2の3乗)倍に、右シフトした結果は1/8になっている事が確認できると思います(−12.5 が切り捨てされて -13 になっています)。

次に注目していただきたいのは符号ありに対する2進数の表示結果です。右シフト演算で解説した通り、右シフト演算では演算対象が「符号なし」か 「符号あり」によって、シフトにより空いてしまったビットに格納される値が下記のように動きが異なります。

  • 符号なし:”0″ が格納される
  • 符号あり:”最上位ビット” の値が格納される

valueC がシフト前の値で、valueD が3ビット右シフト後の値で、シフトにより空いたビットには最上位ビットの値 “1” が格納されている事が確認できると思います。

もし「符号あり」でも「符号なし」と同じ動きであれば、3ビット右シフト結果は下記のようになってしまいます。

0b00011110

これは10進数表示すると “30” になり、”-100″ を 1/8した結果と合いませんね。つまり、右シフトではシフト時に「2のX乗」分の1の計算ができるように、符号ありと符号なしとで動きを変えられているのです。2進数だけに注目すると動きが異なるので分かりにくいのですが、右シフトで「符号なし」と「符号あり」とで動きが異なる事でこのようなメリットがあるんです。

2のべき乗の値の倍数かどうかを調べる

ある値が2のべき乗の倍数かどうかは、その値と「2のX乗 – 1」の各ビットで AND 演算を行う事で調べる事が可能です。これはプログラムの高速化でも紹介した方法ですね。

例えば2の3乗である “8” を2進数で表示すると下記のようになります。

0b00001000

第3ビットのみが “1” ですね。第3ビットよりも下位のビットが “1” の場合、その数は必ず8の倍数にはなりません。逆に第3ビットよりも上位のビットのみが “1” の場合はどんな数でも8の倍数になります。これは “8” に限った話ではなく、「2のX乗」の値全てに対して言える事です。

なので、「2のX乗」の倍数であるかどうかは第Xビットよりも下位のビットが全て “0” であるかどうかで判定する事ができます。さらに、下位のビットが全て “0” であるかどうかは、その下位のビットを全て “1” にした値(つまり「2のX乗 – 1」)の各ビットと AND 演算を行い、結果が “0” になるかどうかで判断できます(1つでも “1” があれば AND 演算結果が “0” にならない)。

ある値が2のべき乗の倍数であるかどうかを調べるプログラムは下記のようになります。

int main(void){

  char valueA;
  char valueB;

  valueA = 96;
  valueB = -128;

  /* valueAが64の倍数かどうかを判断 */
  if(valueA & 63){
    printf("valueAは64の倍数ではありません\n");
  } else {
    printf("valueAは64の倍数です\n");
  }

  /* valueBが64の倍数かどうかを判断 */
  if(valueB & 63){
    printf("valueBは64の倍数ではありません\n");
  } else {
    printf("valueBは64の倍数です\n");
  }

  return 0;
}

表示結果は下記のようになります。

valueAは64の倍数ではありません
valueBは64の倍数です

スポンサーリンク

まとめ

このページではビット演算について解説しました。ビット演算を用いることによりプログラムの省メモリ化や高速化が可能です。

組み込み製品向けのプログラミングを行なったり、よりC言語の特徴を活かすためにはビット演算の知識は必須になります。

是非この機会にビット演算というものがあること、C言語でビット演算を行う方法をマスターしてみてください!

このビット演算・論理演算を用いた四則演算の実現方法の解説ページを作成しました。ビット演算・論理演算の理解がさらに深まりますので是非読んでみてください。

論理演算で四則演算を行う方法の解説ページのアイキャッチ 論理演算(ビット演算)を使って四則演算を行う方法を解説

オススメの参考書(PR)

C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!

まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。

  • 参考書によって、解説の仕方は異なる
  • 読み手によって、理解しやすい解説の仕方は異なる

ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?

それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。

なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。

特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。

もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!

入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示