C言語編 第49章 ビット演算

先頭へ戻る

この章の概要

この章の概要です。

ビット演算

計算をビット単位で行う方法があると、効率的な演算が行えることがあります。 このような演算を、ビット演算と呼びます。

ビット演算では、+、-、*、/ といった演算子は使いません。 代わりに、ビット演算子を使います。 この章では、C言語に用意されているすべてのビット演算子を取り上げます。

なお、ビット演算子が適用できるのは、整数型に対してだけです。 また、強制ではありませんが、符号無しの整数型に限って使うようにした方がトラブルが少なくて済みます

符号無しの整数型に限った方が良い理由は、符号付きの整数型の値を表現するとき、符号のための専用ビットを持つ環境が多いためです。このビットを巻き込んでビット演算を行うと、意図どおりの結果が得られないことがあります。

ビット積(AND)

ビット積(AND)は、2つのビットを比較して、両者がともに 1 のときにだけ、結果が 1 となるビット演算です。いずれか一方、もしくは両方ともが 0 であれば、結果は 0 になります。

ビット「積」なので、乗算の考え方をしています。 「1 * 1」なら「1」になるし、「1 * 0」のように一方だけでも 0 なら 0 になるということです。

ビット積の演算子は & です。

整数型の値 & 整数型の値

いつものように、通常の算術型変換(第21章)が行われ、変換後の型が結果の型になります。

幾つか試してみましょう。

#include <stdio.h>

void andTest(unsigned int a, unsigned int b);

int main(void)
{
    andTest( 0x0f, 0x01 );
    andTest( 0x0f, 0x0f );
    andTest( 0x0f, 0x00 );
    andTest( 0x0f, 0xaa );
    andTest( 0x0f, 0xff );

    return 0;
}

void andTest(unsigned int a, unsigned int b)
{
    printf( "0x%02x & 0x%02x => 0x%02x\n", a, b, (a & b) );
}

実行結果

0x0f & 0x01 => 0x01
0x0f & 0x0f => 0x0f
0x0f & 0x00 => 0x00
0x0f & 0xaa => 0x0a
0x0f & 0xff => 0x0f

ビット演算を使う場合、ソースコード上でも2進数で記述できると分かりやすいのですが、C言語ではそれができませんから、代わりに16進数で記述することが多いです。

実行結果を検討してみましょう。 まず、左辺側は常に 0x0f に固定してあります。これは 2進数では「00001111」です。

最初のパターンでは 0x01 とのビット積を取っています。これは「00001111」&「00000001」ということです。どちらか一方だけでも 0 になっているビットは、結果も 0 になりますから、「00000001」が結果になります。他もパターンも同様に考えられます。

5つのパターンの結果をそれぞれよく観察してみると、右辺側の上位 4ビットにあった 1 が、すべて 0 に置き換わったことが分かります。そして、下位 4ビットの並びには一切の変化がありません。

これがビット積の特徴で、最も重要な使い方を示しています。つまり、0 にしたい部分を 0 に、元のまま残しておきたい部分を 1 にしたビット列を用意し、そのビット列と対象のビット列とのビット積を取るのです。これは一般に「マスクを取る」と呼ばれる手法です。

もし、下位4ビットを 0 にしたければ、「11110000」とのビット積を取ればよいですし、下位2ビットを 0 にしたいならば、「11111100」とのビット積を取ればよいということです。もっと複雑に、偶数ビットだけを 0 にしたいのなら「01010101」とビット積を取れば実現できます。


また、ビット積は、あるビットに 1 が立っているかを調べるという目的にも使えます。例えば、16ビットの整数の最上位ビットが 1 かどうか調べるには、次のようにします。

if( num & 0x80 ){
    /* 最上位ビットは 1 */
}

最上位ビットが 1 であれば、0x80 つまり「10000000」とビット積を行った結果は「10000000」になります。そうでなければ「00000000」となります。前者の結果を得た場合、それは 0以外の値なので真ですし、後者の結果を得た場合、それは 0 なので偽です。

ビット和(OR)

ビット和(OR)は、2つのビットを比較して、一方だけでも 1 のときに、結果が 1 となるビット演算です。両方ともが 0 であれば、結果は 0 になります。

ビット「和」なので、加算の考え方をしています。「0 + 0」では「0」のままですが、「1 + 0」のように一方だけでも 1 なら 1 になります。「1 + 1」だけ違いますが、上限値で頭打ちしていると考えればよいでしょうか。

ビット和の演算子は | です。

整数型の値 | 整数型の値

いつものように、通常の算術型変換(第21章)が行われ、変換後の型が結果の型になります。

幾つか試してみましょう。

#include <stdio.h>

void orTest(unsigned int a, unsigned int b);

int main(void)
{
    orTest( 0x0f, 0x01 );
    orTest( 0x0f, 0x0f );
    orTest( 0x0f, 0x00 );
    orTest( 0x0f, 0xaa );
    orTest( 0x0f, 0xff );

    return 0;
}

void orTest(unsigned int a, unsigned int b)
{
    printf( "0x%02x | 0x%02x => 0x%02x\n", a, b, (a | b) );
}

実行結果

0x0f | 0x01 => 0x0f
0x0f | 0x0f => 0x0f
0x0f | 0x00 => 0x0f
0x0f | 0xaa => 0xaf
0x0f | 0xff => 0xff

前の項での andTest関数と同じ値を使って、今度はビット和演算を行っています。

例えば、1つ目のパターンでは、「00001111」と「00000001」のビット和演算です。両方のビットが 0 のところだけ 0 のままで、ほかのビットは 1 になりますから、「00001111」という結果が得られます。
「00001111」|「10101010」であれば、「10101111」になります。この結果を観察すると、左辺と右辺の値の中で、1 になっている部分が合体したような状態です。

ビット和は、任意のビットに 1 を立てるような目的で使うことができます。例えば、上位 4ビットにまとめて 1 を立てたいときには、「11110000」とのビット和を取ればよい訳です。同様に、偶数ビットをすべて 1 にしたければ、「10101010」とのビット和を取れば実現できます。

ビット否定(NOT)

ビット否定(NOT)は、ビットが 0 になっている箇所を 1 に、1 になっている箇所を 0 にする演算です。

ビット否定の演算子は ~ です。 ビット演算子の中で唯一、オペランドが1つだけの演算子です。

~整数型の値

オペランドの型は、汎整数拡張(第21章)によって拡張され、結果の型もこれと同じになります。

幾つか試してみましょう。

#include <stdio.h>

void notTest(unsigned int a);

int main(void)
{
    notTest( 0x00 );
    notTest( 0x01 );
    notTest( 0x07 );
    notTest( 0xF0 );
    notTest( 0xFF );

    return 0;
}

void notTest(unsigned int a)
{
    printf( "~0x%08x => 0x%08x\n", a, ~a );
}

実行結果

~0x00000000 => 0xffffffff
~0x00000001 => 0xfffffffe
~0x00000007 => 0xfffffff8
~0x000000f0 => 0xffffff0f
~0x000000ff => 0xffffff00

ビット否定は、すべてのビット(1つ1つのビット)を、0 なら 1 に、1 なら 0 にひっくり返します。これはシンプルで分かりやすいと思います。

特徴的な性質を1つ挙げておきます。
あるビット列 a に対して、ビット否定演算を行った結果 ~a があるとき、「a | ~a」のように、OR演算で結合すると、全てのビットが 1 になります。

また、「~0」のように表記することで、すべてのビットが 1 の整数値を得られることが利用されるケースがあります。例えば、次の2つの宣言は同じ意味でしょうか?

unsigned int n1 = 0xffffffff;
unsigned int n2 = ~0;

n1 の方は、unsigned int が 32ビットであることを想定してしまっています。もし 64ビットの環境だったら、上位の 32ビットは 0 になります。対して n2 の方は、unsigned int型の大きさがいくらであっても、必ず全ビットが 1 の数値で初期化することができます。

排他的論理和(XOR)

排他的論理和(XOR、EOR)は、2つのビット同士を比較して、いずれか一方だけが 1 のときに結果が 1 になるビット演算です。ここではビット単位の話をするので、ビット単位の排他的論理和とか、排他的ビット和と呼ぶ方が適切ですが、単に排他的論理和と呼ぶことも多いです。

排他的論理和の演算子は ^ です。

整数型の値 ^ 整数型の値

いつものように、通常の算術型変換(第21章)が行われ、変換後の型が結果の型になります。

幾つか試してみましょう。

#include <stdio.h>

void xorTest(unsigned int a, unsigned int b);

int main(void)
{
    xorTest( 0x0f, 0x01 );
    xorTest( 0x0f, 0x0f );
    xorTest( 0x0f, 0x00 );
    xorTest( 0x0f, 0xaa );
    xorTest( 0x0f, 0xff );

    return 0;
}

void xorTest(unsigned int a, unsigned int b)
{
    printf( "0x%02x ^ 0x%02x => 0x%02x\n", a, b, (a ^ b) );
}

実行結果

0x0f ^ 0x01 => 0x0e
0x0f ^ 0x0f => 0x00
0x0f ^ 0x00 => 0x0f
0x0f ^ 0xaa => 0xa5
0x0f ^ 0xff => 0xf0

排他的論理和では、比較するビットが同一の部分だけが 0 になります。最初の4つの実行例では、互いに上位 4ビットが 0 なので、結果の上位 4ビットもすべて 0 になっています。

排他的論理和には、2度繰り返すと、元の値に戻るという特性があります。

#include <stdio.h>

void xorTest(unsigned int a, unsigned int b);

int main(void)
{
    xorTest( 0x0f, 0x01 );
    xorTest( 0x0f, 0x0f );
    xorTest( 0x0f, 0x00 );
    xorTest( 0x0f, 0xaa );
    xorTest( 0x0f, 0xff );

    return 0;
}

void xorTest(unsigned int a, unsigned int b)
{
    unsigned int ans = a ^ b;

    printf( "0x%02x ^ 0x%02x => 0x%02x\n", a, b, ans );
    printf( "0x%02x ^ 0x%02x => 0x%02x\n", ans, b, (ans ^ b) );
    printf( "\n" );
}

実行結果

0x0f ^ 0x01 => 0x0e
0x0e ^ 0x01 => 0x0f

0x0f ^ 0x0f => 0x00
0x00 ^ 0x0f => 0x0f

0x0f ^ 0x00 => 0x0f
0x0f ^ 0x00 => 0x0f

0x0f ^ 0xaa => 0xa5
0xa5 ^ 0xaa => 0x0f

0x0f ^ 0xff => 0xf0
0xf0 ^ 0xff => 0x0f

このように、「a ^ b」の結果 c に対して、「c ^ b」とすると a に一致します。この特性は、ごく簡易的な暗号の仕組みとして利用できます(アルゴリズムとデータ構造編【その他】第3章参照

シフト演算

シフト演算は、ビット列をずらす操作を行います。

シフト演算には、左方向に(上位桁に向かって)ずらす左シフト演算と、右方向に(下位桁に向かって)ずらす右シフト演算とがあります。それぞれ、<<演算子と、>>演算子を使います。

整数型の値 << 整数型の値
整数型の値 >> 整数型の値

左オペランドのビット列を、右オペランドの値の数だけずらします。右オペランドに負数を指定した場合の動作は未定義です。

ほとんど気にする必要はありませんが、型変換のルールは少し複雑です。2つのオペランドのそれぞれに、汎整数拡張(第21章)が適用され、通常の算術型変換(第21章)は適用されません。結果の型は、左のオペランドと同じになります。

「10010111 << 3」であれば、各ビットが左方向に 3ビットずらされ「10111000」という結果になります。左シフトした結果、空きになった右側のビットには 0 が埋められます。

一方、「10010111 >> 3」であれば、各ビットが右方向に 3ビットずらされ「00010010」という結果になります。ただし、これが保証されるのは、符号無し整数型の場合に限られます。これは、冒頭で解説した通りです。ビット演算を行うときには、符号無し整数型を使いましょう。
もし、符号付き整数で、負数の値を右シフトした場合、空きになった左側のビットに 0 が入るのか 1 に入るのかは、コンパイラが決定することになっています

幾つか試してみましょう。

#include <stdio.h>

void lShiftTest(unsigned int a, unsigned int b);
void rShiftTest(unsigned int a, unsigned int b);

int main(void)
{
    lShiftTest( 0x0f, 1 );
    lShiftTest( 0x0f, 4 );
    lShiftTest( 0x0f, 5 );
    rShiftTest( 0x0f, 1 );
    rShiftTest( 0x0f, 4 );
    rShiftTest( 0x0f, 0 );

    return 0;
}

void lShiftTest(unsigned int a, unsigned int b)
{
    printf( "0x%02x << %u => 0x%02x\n", a, b, (a << b) );
}

void rShiftTest(unsigned int a, unsigned int b)
{
    printf( "0x%02x >> %u => 0x%02x\n", a, b, (a >> b) );
}

実行結果

0x0f << 1 => 0x1e
0x0f << 4 => 0xf0
0x0f << 5 => 0x1e0
0x0f >> 1 => 0x07
0x0f >> 4 => 0x00
0x0f >> 0 => 0x0f

5ビットの左シフトをした結果は桁が増えていますが、これは 32ビットの int型を使っているためです。16ビットの変数であれば、上位の桁はカットされて、0xe0 になります。

右シフトの場合、下位のビットが削られていくのが分かります。空いた上位ビットには、0 が埋められていますが、先ほど説明したように、左辺側が負数の場合には、保証されません。


シフト演算は、乗算や除算の代わりとなることがあります。2進法の性質からいって、1ビット上位に動くと、10進法での 2倍に相当するので、1ビット左シフトするたびに 2倍、1ビット右シフトするたびに 2分の1 を意味します。

#include <stdio.h>

int main(void)
{
    unsigned int num;

    num = 1;

    num = num << 1;
    printf( "%u\n", num );
    num = num << 1;
    printf( "%u\n", num );
    num = num << 1;
    printf( "%u\n", num );

    num = num >> 1;
    printf( "%u\n", num );
    num = num >> 1;
    printf( "%u\n", num );
    num = num >> 1;
    printf( "%u\n", num );

    return 0;
}

実行結果

2
4
8
4
2
1

シフト演算は、乗算や除算よりも高速に行えることもあり、昔はよく最適化の一環で使われることがありました。現代のコンパイラは優秀なので、このような手法を人間が手動で行うことにはほぼ意味がありません。「*8」よりも「<< 3」の方が読みやすいなどということは普通は無いので、素直に乗算や除算を使うと良いです。

複合代入演算子

ここまでに紹介した各演算子には、複合代入演算子(第27章)も存在しています。 すなわち、

があります。ビット否定演算子だけは、オペランドが1つの演算子なので、複合代入演算子はありません。


練習問題

問題① unsigned int型の変数num に 0 が格納されていたら 1 に、1 が格納されていたら 0 に変換する処理を、ビット演算を使って書いてください。

問題② ある正の整数に、1 が立っているビットが幾つあるかを調べるプログラムを作成して下さい。

問題③ 次の関数は何をしているでしょうか。

int func(unsigned int a, unsigned int b)
{
    return ( ( a | b ) & ~( a & b ) );
}

問題④ 標準入力から受け取った 10進数の正の整数を、2進法で標準出力へ出力するプログラムを作成して下さい。


解答ページはこちら

参考リンク

更新履歴

'2018/3/24 全面的に文章を見直し、修正を行った。

'2018/2/21 文章中の表記を統一(bit、Byte -> ビット、バイト)

'2012/5/25 「XOR演算」の項から、アルゴリズムとデータ構造編へのリンクを追加。

'2011/2/6 新規作成。





前の章へ(第48章 理解の定着・小休止⑤)

次の章へ(第50章 列挙型)

C言語編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS