【C言語入門】ビット演算子、シフト演算子の使い方(使い道も解説)

こんにちは!フリーライターの中井です。

C言語には2進数のビット単位で計算するビット演算があります。この記事では、

  • ビット演算とは
  • ビット演算子とは
  • ビット演算子の使い方

という基本的な内容から、シフト演算の使い方などの応用的な使い方に関しても解説していきます。

ビット演算とは

ビット演算とは、2進数の0か1で表現するビット単位で計算することです。フラグの確認でよく使われていたり、PCの仕組みを理解するためには欠かせない項目です。

C言語では、のちに説明するビット演算子を使うことによって簡単に計算できるので仕組みまで理解しておきましょう。

ビット演算の使い道

PCは2進数で計算する方が単純な計算式になるため、処理速度が非常に速くなる傾向があります。数百万から数千万画素の画像を加工する場合などは、時間がかかる処理をビット演算で計算することも多いです。

また、メモリ使用量も少ない傾向がありますので、マイコンのようにメモリが少ないハードウェアを使う場合はビット演算で計算することも多いです。

ビット演算子とは

ビット演算子とは、ビットを計算させるための演算子です。我々が計算するときに思い浮かべる足し算の「⁺」などのようなものです。ビット演算子では少し特殊なので順番に見ていきましょう。

ビット演算子の種類
&AND。両方1のときに1、それ以外は0にする演算子
|OR。両方0のときのみ0。それ以外は1にする演算子
^XOR。両方が異なる値の時に1。それ以外は0にする演算子。
~NOT。0は1、1は0に反転させる演算子。
>>シフト演算で右へずらす。10進数で言う桁移動のようなもので、すべてのビットの桁を移動する。符号を考慮するかしないかで演算の方法が異なる。
<<シフト演算で左へずらす。

詳細な使い方をプログラムと一緒に学んでいきましょう。

ビット演算子の使い方

ここでは、C言語のコンパイラにGCCを使っているので、"0b1010"のように数値の先頭に"0b"をつけて2進数を扱っています。環境によっては"0b"をつけても2進数として扱えないので、注意してください。

AND(&)演算子の使い方

&演算子はA & Bと表記され、AもBも1の時に1、それ以外は0にする演算子です。その様子をプログラムで見て確認しましょう。なお、実行結果を表示するためにprintf関数を通常は使いますが、printf関数では2進数を表示することができません。

ですので、2進数表示用の関数を作って演算結果を表示しています。

#include <stdio.h>

// 2進数表示用の関数
void printBi(int num) {
    int len = 4;
    int bit[8];
    int x;
    
    for(int i = 0; i < len; i++) {
        x = 1 << i;
        x = num & x;
        bit[len - i - 1] = x >> i;
    }
    
    printf("0b");
    for(int i = 0; i < len; i++) {
        printf("%d", bit[i]);
    }
}

int main(void) {
    int num1, num2, result;
 
    num1 = 0b0101;
    num2 = 0b0011;
    result = num1 & num2;
    
    // 演算結果を2進数と10進数で表示
    printf("2進数:");
    printBi(result);
    printf("(10進数:%d)\n", result);
    
    return 0;
}

実行結果:

2進数:0b0001(10進数:1)

上記のプログラムでは、num1とnum2をAND演算した結果を変数resultに代入しています。num1とnum2の同じ桁のビットを比較して、両方が1のときだけ1になっていることがプログラムから確認できます。

なお、2進数表示用の関数printBiを簡単に説明しておきます。変数xを使ってどの桁の値が1かどうか調べています。まず、式「1 << i」で桁を順にシフトしています。

演算子「<<」は左シフト演算子と言って、後ほどシフト演算で詳しく解説します。次に、AND演算子「&」を使って調べたい桁の値が1であれば1を、そうでなければ0を返すようにしています。返した値を右シフト演算子「>>」を使って1桁目までシフトさせ、結果を配列bitの要素に格納しています。

そうすると、配列bitの各要素に各桁の値が格納されますので、それを1つずつつなげて表示しています。

OR (|)演算子の使い方

|演算子はA | Bと表記され、AとBの両方が0でなら0を返し、それ以外は1にする演算子です。その様子をプログラムで見て確認しましょう。

#include <stdio.h>

// 2進数表示用の関数
void printBi(int num) {
    int len = 4;
    int bit[8];
    int x;
    
    for(int i = 0; i < len; i++) {
        x = 1 << i;
        x = num & x;
        bit[len - i - 1] = x >> i;
    }
    
    printf("0b");
    for(int i = 0; i < len; i++) {
        printf("%d", bit[i]);
    }
}

int main(void) {
    int num1, num2, result;
 
    num1 = 0b0101;
    num2 = 0b0011;
    result = num1 | num2;
    
    // 演算結果を2進数と10進数で表示
    printf("2進数:");
    printBi(result);
    printf("(10進数:%d)\n", result);
    
    return 0;
}

実行結果:

2進数:0b0111(10進数:7)

上記のプログラムでは、num1とnum2をOR演算した結果を変数resultに代入しています。num1とnum2の同じ桁のビットを比較して、両方が0でなら0、それ以外は1になっていることがプログラムから確認できます。

XOR(^)演算子の使い方

^演算子はA ^ Bと表記され、AとBが異なる場合に1、AとBは同じ場合に0を返す演算子です。その様子をプログラムで見て確認しましょう。

#include <stdio.h>

// 2進数表示用の関数
void printBi(int num) {
    int len = 4;
    int bit[8];
    int x;
    
    for(int i = 0; i < len; i++) {
        x = 1 << i;
        x = num & x;
        bit[len - i - 1] = x >> i;
    }
    
    printf("0b");
    for(int i = 0; i < len; i++) {
        printf("%d", bit[i]);
    }
}

int main(void) {
    int num1, num2, result;
 
    num1 = 0b0101;
    num2 = 0b0011;
    result = num1 ^ num2;
    
    // 演算結果を2進数と10進数で表示
    printf("2進数:");
    printBi(result);
    printf("(10進数:%d)\n", result);
    
    return 0;
}

実行結果:

2進数:0b0110(10進数:6)

上記のプログラムでは、num1とnum2をXOR演算を行い、その結果を変数resultに代入しています。num1とnum2の同じ桁のビットを比較して、両方が異なるときだけ1になっていることがプログラムから確認できます。

NOT(~)演算子の使い方(ビット反転)

~演算子は、~Aと表記されAが1の時に0、Aが0のときには1にする演算子です。これをビット反転と呼びます。ビット反転の場合は負の値も考慮する必要があるので、char型を使ってわかりやすく8bitで解説していきます。

int型だと16bitか32bitか環境によって異なり、演算結果が変わってくるからです。実際プログラムで確認してみましょう。

#include <stdio.h>

// 2進数表示用の関数
void printBi(char num) {
    int len = 8;
    int bit[8];
    int x;
    
    for(int i = 0; i < len; i++) {
        x = 1 << i;
        x = num & x;
        bit[len - i - 1] = x >> i;
    }
    
    printf("0b");
    for(int i = 0; i < len; i++) {
        printf("%d", bit[i]);
    }
}

int main(void) {
    int num;
    int result;
 
    // 0を1に反転
    num = 0b00000000;
    result = ~num;
    
    // 演算結果を2進数と10進数で表示
    printf("2進数:");
    printBi(result);
    printf("(10進数:%d)\n", result);
    
 
    // 1を0に反転
    num = 0b00001111;
    result = ~num;
    
    // 演算結果を2進数と10進数で表示
    printf("2進数:");
    printBi(result);
    printf("(10進数:%d)\n", result);
    
    return 0;
}
2進数:0b11111111(10進数:-1)
2進数:0b11110000(10進数:-16)

上記のプログラムでは、2進数0と2進数1111を反転させて表示しています。0だったものが1に、1だったものが0にビット反転していることが確認できます。ちなみに、符号つきの2進数では一番左はしは符号を表します。

これを補数と言います。0の場合は正の数、1の場合は負の数を表します。したがって、10進数で正の数を2進数でビット反転させると10進数ではマイナスの値になります。負の値の2進数は、すべてのビットを反転させ、1を加えて作ります。

シフト演算の使い方

応用的な使い方として、シフト演算の使い方を解説します。シフト演算とはビットの桁移動です。int型だと16bitか32bitかが環境によって変わるので、char型を使ってわかりやすく8bitで解説していきます。

2進数を基準にして考えるので、左にシフトすると値が2倍になり、右にシフトすると値が1/2になります。また、シフト演算で桁があふれた場合には正しい値にはならないので注意が必要です。

左シフト(<<)演算子

左シフト演算子(<<)はA << 1と表記され、2進数で表記されたAを左へ1ビット桁移動をする演算子です。左シフトのときは、左側にあふれたビットは削除され、右側に0が追加されます。実際プログラムで左シフト演算を確認してみましょう。

#include <stdio.h>

// 2進数表示用の関数
void printBi(char num) {
    int len = 8;
    int bit[8];
    int x;
    
    for(int i = 0; i < len; i++) {
        x = 1 << i;
        x = num & x;
        bit[len - i - 1] = x >> i;
    }
    
    printf("0b");
    for(int i = 0; i < len; i++) {
        printf("%d", bit[i]);
    }
}

int main(void) {
    char num, result;
    
    // 10進数の14を2進数で表記
    num = 0b00001110;
    printf("%d\n", num);
    
    // numを1ビット左へシフト
    result = num << 1;
    
    // 演算結果を2進数と10進数で表示
    printf("2進数:");
    printBi(result);
    printf("(10進数:%d)\n", result);
    
    return 0;
}

実行結果:

14
2進数:0b00011100(10進数:28)

上記のプログラムはnumに2進数『0000 1110』が入り、numを1ビット左シフトを行った結果を変数resultに代入し表示をしています。この2進数を左へ1桁シフトすると、左へ桁をずらすので『0001 1100』になります。右端には0が追加されています。

これを10進数に直すと、元の値『14』を2倍した値の『28』になります。

右シフト(>>)演算子

右シフト演算子(>>)はA >> 1と表記され、2進数で表記されたAを右へ1ビット桁移動をする演算子です。右シフトのときは、右側にあふれたビットは削除され、左側に0が追加されます。実際プログラムで右シフト演算を確認してみましょう。

#include <stdio.h>

// 2進数表示用の関数
void printBi(char num) {
    int len = 8;
    int bit[8];
    int x;
    
    for(int i = 0; i < len; i++) {
        x = 1 << i;
        x = num & x;
        bit[len - i - 1] = x >> i;
    }
    
    printf("0b");
    for(int i = 0; i < len; i++) {
        printf("%d", bit[i]);
    }
}

int main(void) {
    char num, result;
    
    // 10進数の14を2進数で表記
    num = 0b00001110;
    printf("%d\n", num);
    
    // numを1ビット右へシフト
    result = num >> 1;
    
    // 演算結果を2進数と10進数で表示
    printf("2進数:");
    printBi(result);
    printf("(10進数:%d)\n", result);
    
    return 0;
}

実行結果:

14
2進数:0b00000111(10進数:7)

上記のプログラムはnumに2進数『0000 1110』が入り、numを1ビット右シフトを行った結果を変数resultに代入し表示をしています。右シフトは、右に桁を移動させます。

『0000 1110』を右に1桁シフトするので、『0000 0111』になります。右端の桁は削除され、左端には0が追加されます。これを10進数に直すと、元の値『14』を1/2にした値の『7』になります。

負の値のシフト演算

シフト演算には、論理シフトと算術シフトがあります。論理シフトは単純にすべてのビットをシフトします。算術シフトの場合には符号が変わらないように、最上位ビット以外をシフトします。

負の数のシフト演算では、右シフトの場合のみシフトされた分左側に1が追加されることに注意してください。実際のプログラムを見てみましょう。

#include <stdio.h>

// 2進数表示用の関数
void printBi(char num) {
    int len = 8;
    int bit[8];
    int x;
    
    for(int i = 0; i < len; i++) {
        x = 1 << i;
        x = num & x;
        bit[len - i - 1] = x >> i;
    }
    
    printf("0b");
    for(int i = 0; i < len; i++) {
        printf("%d", bit[i]);
    }
}

int main(void) {
    char num, result;
    // 負の値-14を右シフトした場合
    num = 0b11110010;
    printf("%d\n", num);
 
    result = num >> 1;
    
    // 演算結果を2進数と10進数で表示
    printf("2進数:");
    printBi(result);
    printf("(10進数:%d)\n", result);
    
    return 0;
}

実行結果:

-14
2進数:0b11111001(10進数:-7)

上記のプログラムでは、変数numに『1111 0010』が入っています。負の値の場合は左端が符号を表すビットなので、右シフトしても符号が変わらないように左端に1が入ります。そのため、『1111 1001』となります。

これを10進数に直すと、元の値『-14』を1/2にした値の『-7』になります。

まとめ

この記事ではビット演算について解説しました。ビット演算には、AND演算子、OR演算子、XOR演算子、NOT演算子、右シフト演算子、左シフト演算子がありました。それぞれの違いをしっかり理解しましょう。

特にシフト演算の場合、正数か負数かで考え方が異なります。もし、ビット演算について忘れてしまったらこの記事を確認してくださいね!

LINEで送る
Pocket

「プログラミング、右も左もわからない…」という方にオススメ

当プログラミングスクール「侍エンジニア塾」では、これまで6000人以上のエンジニアを輩出してきました。

その経験を通してプログラミング学習に成功する人は、「目的目標が明確でそれに合わせた学習プランがあること」「常に相談できる人がそばにいること」「自己解決能力が身につくこと」この3つが根付いている傾向を発見しました。

侍エンジニア塾は上記3つの成功ポイントを満たすようなサービス設計に磨きをかけております。

cta_under_bnr

「自分のスタイルや目的に合わせて学習を進めたいな」とお考えの方は、ぜひチェックしてみてください。

書いた人

中井

中井

フリーライター。
主に美容系、ライフスタイル系含めこれまでに500本以上執筆。
趣味はオンラインゲームにフリーソフトの情報収集。

プログラミンとの出会い
-----------------------------------------------------------------------------------------
PCとプログラミングは大学からです。
それまでは、PCは触ると壊れると思い込んでました。

大学の授業で面白そうな理由でJavaを専攻。
全然授業についていけず、後ろの席の子に課題をやってもらってました。

そんなプログラミング音痴な状態で社会人を迎え、先輩からVBAがオススメと言われVBAを職場の人から教えてもらい習得。
その後、自分の業務に携わるものおは全てVBAを組みました。

会社を退職後、再度Javaを勉強する機会に恵まれ、大学でわからなかったことが一気に理解できるように。
念願のAndroidアプリでTwitterのクライアントアプリを作れるまでになりました。
以後、文章の修行の傍らJavaの最新情報を追う日々が続いています。