ビット単位の処理 | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、前のページで取り上げた1バイト単位で処理を行う話をさらに押し進めて、1ビット単位で処理を行う方法を取り上げます。ビット単位の処理は、必要なデータ量を極限まで小さくするために役立ちます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



ビット単位の処理

配列とポインタ」のページでは、データを1バイト単位で取り扱うため、バイト列という考え方が登場しました。今度はさらに単位を小さくして、1ビット単位での取り扱い方を学ぶことにします。ビットは情報の最小単位なので、これ以上を分解して扱うことはできません。

ビット単位で行う処理とは、あるビットに 0 か 1 を設定する、あるビットが 0 なのか 1 なのかを調べるといったことです。そのために使用する演算(ビット演算 (bitwise operation))に、ビット単位ANDビット単位ORビット単位XORビット単位NOTシフト演算といったものがあります。このページでそれぞれ解説します。

n進数の整数リテラル

ビット演算を解説する前に、整数リテラルを 10進数以外で表記する方法を紹介しておきます。1ビットは、2進数の 1桁にあたるため、2進数で表記できると分かりやすいです。

整数リテラルに以下のプリフィックスを付加することで、10進数以外を使って表記できます。

プリフィックス 基数
0b または 0B 2進数
0 8進数
0x または 0X 16進数

実行できるプログラムは次のようになります。

#include <iostream>

int main()
{
    int num10 {1000};
    int num2 {0b1111101000};
    int num8 {01750};
    int num16 {0x3E8};

    std::cout << num10 << "\n"
              << num2 << "\n"
              << num8 << "\n"
              << num16 << "\n"
              ;
}

実行結果:

1000
1000
1000
1000

何進数で表記するかは、表記上の問題であって、表している値には違いがないことを理解してください。このサンプルでは、10000b1111101000017500x3E8 という4つの整数リテラルが登場していますが、これはすべて、10進数の 1000 と同じ値です。実際、出力してみるとすべて 1000 になっています。

プリフィックスがあっても、桁区切り文字(「int型の限界」のページを参照)を入れることはできます(たとえば 0b11'1110'1000 のように)。

配列とポインタ」のページで取り上げたとおり、std::oct や std::hex というマニピュレータを使えば、8進数や 16進数で出力できます(残念なことに 2進数で出力する簡単な方法はありません)。

std::oct や std::hex を使うだけでは、整数リテラルのプリフィックス(00x0X)と同じものが付加されませんが、std::showbase を併用すると付加されます。std::noshowbase を使うと、プリフィックスが付加されない状態に戻ります。

#include <iostream>

int main()
{
    int num8 {01750};
    int num16 {0x3E8};

    std::cout << std::showbase
              << std::oct << num8 << "\n"
              << std::hex << num16 << "\n"
              << std::noshowbase
              << std::oct << num8 << "\n"
              << std::hex << num16 << "\n"
              ;
}

実行結果:

01750
0x3e8
1750
3e8

2進数で出力するには

どうしても 2進数で出力したければ、std::bitset を使う方法があります。std::bitset を使うには、<bitset> のインクルードが必要です。

#include <bitset>
#include <iostream>

int main()
{
    int num2 {0b11'1110'1000};

    std::cout << std::bitset<16>(num2) << "\n";
}

実行結果:

0000001111101000

std::bitset<ビット数>(値) というかたちで表記します。指定したビット数を使い切らない場合、上位のビットに 0 が埋められるので、これが余計な場合もありますが、知っておくと便利なこともあると思います。

std::bitset は本来、std::vector や std::string のように、データ構造を取り扱うもので、ビット単位の配列を表現します。そうした目的での利用についてはページをあらためて取り上げますが、このサンプルのような使い方も可能です。

整数リテラルの型

以前、「整数型」のページで、整数リテラルの型のルールを説明しました。簡単にいえば、その値を表現できる型を、int -> long -> long long -> 拡張整数型の順番に探して、最初にみつけた型が選ばれるということでした。LLL のように、大きさを指定するサフィックスがある場合は、最小でも long や long long が選ばれることになります。また、符号無しであることを指定する uU が付加されているときは、候補の型がすべて符号付き整数型になります。

整数リテラルにプリフィックスが付いている場合、このルールが変化し、符号付き整数型も候補に加わりますuU のサフィックスがあるなら、符号付き整数型だけが候補になります)。

表にまとめると次のようになります1 2。それぞれの枠の中にある型を順番に調べて、その値を表現できる最初の型が選ばれます。どの型でも表現できない場合はコンパイルエラーです。

プリフィックス サフィックス 型の候補
なし なし int、long、long long、拡張符号付き整数型
なし l または L long、long long、拡張符号付き整数型
なし ll または LL long long、拡張符号付き整数型
なし u または U unsigned int、unsigned long、unsigned long long、拡張符号なし整数型
なし u または U と l または L unsigned long、unsigned long long、拡張符号なし整数型
なし u または U と ll または LL unsigned long long、拡張符号なし整数型
0b、0B、0、0x、0X のいずれか なし int、unsigned int、long、unsigned long、long long、unsigned long long、拡張符号付き整数型、拡張符号なし整数型
0b、0B、0、0x、0X のいずれか l または L long、unsigned long、long long、unsigned long long、拡張符号付き整数型、拡張符号なし整数型
0b、0B、0、0x、0X のいずれか ll または LL long long、unsigned long long、拡張符号付き整数型、拡張符号なし整数型
0b、0B、0、0x、0X のいずれか u または U unsigned int、unsigned long、unsigned long long、拡張符号なし整数型
0b、0B、0、0x、0X のいずれか u または U と l または L unsigned long、unsigned long long、拡張符号なし整数型
0b、0B、0、0x、0X のいずれか u または U と ll または LL unsigned long long、拡張符号なし整数型

ビット単位NOT(ビット単位論理否定)

ではここからビット演算の説明をしていきます。1つ目は、ビット単位NOT(ビット単位論理否定) (bitwise NOT) です。

以前、「要素を探索する」のページで、論理否定という演算(演算子は !)を解説しました。論理否定は、オペランドが true なら false に、false なら true になるというものでした。ビット単位NOT はこれのビット版で、対象のビットが 1 なら 0、0 なら 1 になります。ビットの 0 と 1 が逆になることから、ビット反転 (bit flipping) と呼ぶことがあります。

【上級】正確には、オペランドの1の補数を求める演算と定義されます3

ビット単位NOT の演算子は ~ です。

~

「式」は、整数型か unscoped enum でなければなりません。

なお、演算を行う前に整数拡張(「整数型」のページを参照)が行われ、結果の型は整数拡張後の型です4

実際に試してみます。

#include <bitset>
#include <iostream>

int main()
{
    std::cout << std::bitset<4>(~0b0011u) << "\n"
              << std::bitset<8>(~0b11001100u) << "\n";
}

実行結果:

1100
00110011

ビットが反転されていることが分かります。

ビット単位NOT は、オペランドそのものの値を変更するわけではないので、変数の値をビット反転するときは、結果を変数に書き戻す必要があります。

auto value = 0b1100u;
~value;           // 結果を捨てている。value の値は変わらない
value = ~value;   // value がビット反転する

任意のビットだけを反転させたい場合は、ビット単位XOR を使います。

ここまでのサンプルプログラムでは、~ のオペランドに uサフィックスが付けて符号無し整数型にしています。強制されているわけではないですが、(ビット単位NOT 以外のビット演算も含めて)ビット演算は符号無し整数型に対して使うのが安全です5なぜなら、符号付き整数型には、符号ビット (sign bit)と呼ばれる特別なビットが含まれているためです。普通、最上位のビットのところが符号ビットとして使われており、これが 1 のとき負の整数であることを意味します。符号無し整数型には、符号ビットがなく、すべてのビットを数を表現するために使い尽くします(そのため、符号無し整数型のほうが、扱える上限値が大きくなる)。ビット演算は符号ビットのことを特別扱いせずに操作してしまうため、予想外の結果を生むことがあります。

ビット単位OR(ビット単位論理和)

次は、ビット単位OR(ビット単位論理和) (bitwise OR) です。

以前、「要素を探索する」のページで、論理OR という演算(演算子は ||)を解説しました。論理OR は2つの論理値のいずれか、あるいは両方が true であるときにかぎって結果が true になり、それ以外の場合は false になるという演算でした。ビット単位OR はこれのビット版で、2つのビットのいずれか、あるいは両方が 1 であるときにかぎって結果が 1 になり、それ以外の場合は 0 になるという演算です。

ビット単位OR の演算子は | です。

|

「式」は、整数型か unscoped enum でなければなりません。ビット単位NOT のところでも触れたとおり、符号無し整数型に対して使うのが安全ではあります。

実際に試してみます。0 と 1 の組み合わせは 4通りなので、4ビットの整数同士でビット演算を行って、すべてのパターンを網羅して確認してみます。

#include <bitset>
#include <iostream>

int main()
{
    auto value1 = 0b0011u;
    auto value2 = 0b0101u;

    std::cout << std::bitset<4>(value1 | value2) << "\n";
}

実行結果:

0111

00110101 に対してビット単位OR を行っています。同じ桁にあるビットどうしが演算の対象になります。結果は 0111 です。つまり、以下の表のような結果になったわけで、2つのビットのいずれか、あるいは両方が 1 のときに、結果が 1 になることが分かります。

一方のビット 他方のビット 結果
0 0 0
0 1 1
1 0 1
1 1 1

任意のビットを 1 にする

| は、オペランドの値を変更しているわけではありませんが、結果の値を受け取れば変更できます。

value = value | 0b1100u;

この結果、value の下から 2ビット目と 3ビット目のところが 1 に変更され、ほかのビットには影響を与えません。このように、あるビットを 1 にすることをよく、「ビットを立てる」と表現します。

ビットを 0 にする場合は、ビット単位NOT と、後述するビット単位AND を組み合わせて用います

複合代入演算子 |= を使って、次のように書いても同じ結果になります。

value |= 0b1100u;

【上級】E1 op= E2E1 = E1 op E2 と同じ意味ではありますが、複合代入演算子の場合は E1 が1回しか評価されない点でだけ異なっています6。通常これは好ましい動作です。

次のサンプルプログラムは、偶数番目のビットを立てています。

#include <bitset>
#include <iostream>

int main()
{
    auto value = 0b0110'0011u;

    value |= 0b1010'1010u;
    std::cout << std::bitset<8>(value) << "\n";
}

実行結果:

11101011

10101010 とのビット単位OR によって偶数番目のビットが立ち、奇数番目のビットには何ら影響を与えていません。

このように任意のビットだけを狙って 1 にできるので、情報を少ないメモリで表現することに役立ちます。たとえば、true か false のように2通りの値しか持ちえない情報を bool型で表現すると、最小でも 1バイトの大きさが必要ですが、これを 1ビットに切り詰められます。実際には、どんなに小さい型でも 1バイト(unsigned char型など)あるので、これより小さくはなりませんが、1バイトの中には 8個のビットがあるので、8個の情報が詰め込めます。

// 8個の情報を bool型で表現すると、sizeof(bool) * 8(=最小でも 8バイト)が必要
bool settings[8] {};
settings[2] = true;
settings[7] = true;

// 8個の情報をそれぞれ 1ビットで表現すると、8ビット(=1バイト)に収まる
unsigned char settings {};
settings |= 0b0000'0100u;
settings |= 0b1000'0000u;
settings |= 0b1101'1101u;  // 一気に設定できる利点もある

bool型の大きさは処理系定義であり、1バイトより大きい場合があります(「整数型」のページを参照)。

メモリ領域の最小単位はバイトなので、どうしても 1バイト以上の領域は必要です(「メモリとオブジェクト」のページを参照)。また、メモリアドレスの概念もバイト単位のものなので、1バイトを 8個のビットに分けて情報を詰め込んだとしても、おのおのに別のメモリアドレスが割り当てられることはありません。

ビット演算を活用するためには、ビットを立てられるだけでは不十分で、ビットがどうなっているのかを読み取れなければなりません。そのためには、このあと紹介するビット単位AND を用います。

ビット単位AND(ビット単位論理積)

次は、ビット単位AND(ビット単位論理積) (bitwise AND) です。

以前、「要素を探索する」のページで、論理AND という演算(演算子は &&)を解説しました。論理AND は2つの論理値がともに true であるときにかぎって結果が true になり、それ以外の場合は false になるという演算でした。ビット単位AND はこれのビット版で、2つのビットがともに 1 であるときにかぎって結果が 1 になり、それ以外の場合は 0 になるという演算です。

ビット単位AND の演算子は & です。

&

「式」は、整数型か unscoped enum でなければなりません。ビット単位NOT のところでも触れたとおり、符号無し整数型に対して使うのが安全ではあります。

実際に試してみます。0 と 1 の組み合わせは 4通りなので、4ビットの整数同士でビット演算を行って、すべてのパターンを網羅して確認してみます。

#include <bitset>
#include <iostream>

int main()
{
    auto value1 = 0b0011u;
    auto value2 = 0b0101u;

    std::cout << std::bitset<4>(value1 & value2) << "\n";
}

実行結果:

0001

00110101 に対してビット単位AND を行っています。同じ桁にあるビットどうしが演算の対象になります。結果は 0001 です。つまり、以下の表のような結果になったわけで、2つのビットがともに 1 のときだけ、結果が 1 になることが分かります。

一方のビット 他方のビット 結果
0 0 0
0 1 0
1 0 0
1 1 1

ビットが 1 になっているか調べる

ビット単位AND を使って、あるビットが立っているかどうかを判定できます。たとえば、ある整数が奇数かどうかを次のように調べられます。

#include <iostream>

int main()
{
    auto value1 = 1234u;
    auto value2 = 1235u;

    std::cout << (value1 & 0b1) << "\n"
              << (value2 & 0b1) << "\n";
}

実行結果:

0
1

奇数であれば一番下位のビットが 1 なので、対象の整数 & 0b1 のようにビットAND演算を行うことで、1 が得られるということです。

結果を bool型で得たいかもしれません。その場合は、ビット演算の結果を != 0 で比較します。&& は、等価演算子や関係演算子よりも優先順位が低く設定されているため、ビット演算の式を ( ) で囲む必要があります(「APPENDIX>演算子の一覧」を参照)。

#include <iostream>

int main()
{
    auto value1 = 1234u;
    auto value2 = 1235u;

    std::cout << std::boolalpha
              << ((value1 & 0b1) != 0) << "\n"
              << ((value2 & 0b1) != 0) << "\n";
}

実行結果:

false
true

任意のビットを 0 にする

& は、オペランドの値を変更しているわけではありませんが、結果の値を受け取れば変更できます。複合代入演算子 &= を使って書くこともできます。

value = value & 0b1100u;
value &= 0b1100u;

いずれの方法でも、value と 0b1100 のビット単位AND が取られて、その結果が value に書き戻されます。

【上級】E1 op= E2E1 = E1 op E2 と同じ意味ではありますが、複合代入演算子の場合は E1 が1回しか評価されない点でだけ異なっています6。通常これは好ましい動作です。

ビット単位AND を使って、任意のビットを 0 にできます。ビットを 0 にすることをよく「ビットを降ろす」と表現します。

ビットを 1 にする方法は、ビット単位OR のところで取り上げました。

方法としては、降ろしたいビットだけが 1 になっているビット列を用意して、それをビット単位NOT でビット反転させたものとのあいだで、ビット単位AND を行います。

次のサンプルプログラムは、下位4ビットすべてを 0 にしています。

#include <bitset>
#include <iostream>

int main()
{
    auto value = 0b1110'1011u;

    value &= ~0b0000'1111u;
    std::cout << std::bitset<8>(value) << "\n";
}

実行結果:

11100000

降ろしたいビットは下位4ビットなので、0b00001111 というビット列を用意します。これをビット単位NOT でビット反転してから、ビット単位AND を行います。

結局のところ、ビット反転によって 0b11110000 が作られるので、最初からそれを用意しても同じことではあります。

value &= ~0b0000'1111u;
value &= 0b1111'0000u;  // これでも同じことだが

しかし、ビット単位NOT を使う方法であれば、同じ位置のビットを立てるコードとの共通化が図れます。

constexpr auto lower4bits = 0b0000'1111u;  // 下位4ビットを操作するためのビット列

value |= lower4bits;   // ビットを立てる
value &= ~lower4bits;  // ビットを降ろす

ビット単位XOR(ビット単位排他的論理和)

次は、ビット単位XOR(ビット単位排他的論理和) (bitwise XOR、bitwise exclusive OR) です。

XOR は exclusive OR を省略したものです。EOR や EX-OR のように表現されることもあります。また、排他的(exclusive) でない OR(つまり通常のビット単位OR)のことを、bitwise inclusive OR と表現することがあります。

ビット単位XOR は、2つのビットが同じときに結果が 0 になり、同じでないときに 1 になるという演算です。

ビット単位XOR の演算子は ^ です。

^

「式」は、整数型か unscoped enum でなければなりません。ビット単位NOT のところでも触れたとおり、符号無し整数型に対して使うのが安全ではあります。

実際に試してみます。0 と 1 の組み合わせは 4通りなので、4ビットの整数同士でビット演算を行って、すべてのパターンを網羅して確認してみます。

#include <bitset>
#include <iostream>

int main()
{
    auto value1 = 0b0011u;
    auto value2 = 0b0101u;

    std::cout << std::bitset<4>(value1 ^ value2) << "\n";
}

実行結果:

0110

00110101 に対してビットXOR を行っています。同じ桁にあるビットどうしが演算の対象になります。結果は 0110 です。つまり、以下の表のような結果になったわけで、2つのビットが同じ場合に結果が 0 になることが分かります。

一方のビット 他方のビット 結果
0 0 0
0 1 1
1 0 1
1 1 0

複合代入演算子は ^= です。

任意のビットを反転する

さきほどのサンプルプログラムでは、00110101 の XOR によって、0110 が得られました。この結果をよく観察すると、右オペランドで 1 が立っているビットについてだけ、左オペランドのビットが反転していることが分かります。この結果は、さきほどの表を見ても明らかで、「他方のビット」の列で 1 になっているところだけ、「一方のビット」と「結果」が異なっている(=反転している)ことが分かります。

したがって、反転させたいビットだけを 1 にしたビット列を用意して、ビット単位XOR を行えば、任意のビットを狙って反転できます。

#include <bitset>
#include <iostream>

int main()
{
    auto value = 0b1100'0011u;

    std::cout << std::bitset<8>(value ^ 0b1111'0000) << "\n"
              << std::bitset<8>(value ^ 0b1010'1010) << "\n";
}

実行結果:

00110011
01101001

シフト演算

最後に、シフト演算 (shift operation) を紹介します。

シフト演算は、ビット列を任意のビット数分だけずらす(シフトする)操作です。0011110001111000 になるように、左側にずらすものを左シフト (left shift)、0011110000011110 になるように、右側にずらすものを右シフト (right shift) と呼び分けます。ずらすビット数は指定できます。

左シフトなら上位側のビットが、右シフトなら下位側のビットが失われることになります。反対に、左シフトなら下位側に、右シフトなら上位側に空きができることになりますが、空いたところは 0 で埋められます。

【上級】C++ のシフト演算は、符号ビットの存在を無視しておこなう論理シフトです。算術シフトは用意されていません。

【C++20】消えていったビットが逆側から戻ってくる循環シフト(ビットローテーション)を行う関数が標準ライブラリに追加されています。std::rotl関数8で左循環シフト、std::rotr関数で右循環シフト9が行えます。いずれも、<bit> という標準ヘッダで宣言されています。

左シフトの演算子は <<、右シフトの演算子は >> です。複合代入演算子は <<=>>= です。

<<
>>

【上級】std::cout や std::cin などで使ってきた <<>> も、シフト演算の演算子ですが、動作がオーバーロード(上書き)され、まったく異なる意味が与えられています。

左オペランドのビット列を、右オペランドの値の分だけシフトします。「式」は、整数型か unscoped enum でなければなりません。ビット単位NOT のところでも触れたとおり、符号無し整数型に対して使うのが安全ではあります。

なお、演算を行う前に整数拡張(「整数型」のページを参照)が行われ、結果の型も整数拡張後の型です7

右オペランドが負数の場合や、左オペランドを整数拡張した後のビット数を超える数を指定した場合は、未定義の動作です10

unsigned char value {0b0011'1100};

// 右オペランドが負数なのは、未定義の動作
value <<= -1;

// 左オペランドが int に整数拡張される。
// int の大きさが 32ビットとすると、40ビットのシフトは未定義の動作
value <<= 40;

左オペランドが符号付き整数型で負数の場合、左シフトでは未定義の動作です11。右シフトの場合は処理系定義の動作です12

signed char value {-15};

value <<= 2;  // 未定義の動作
value >>= 2;  // 処理系定義の動作

ややこしいので、符号無し整数型に対して使うようにするのが早いと思います。

乗算や除算の代替

シフト演算が、乗算や除算の代わりとして使われているコードを見かけることがあります。2進数の特性として、ある整数を 1ビット左シフトすると、2倍することと同じ意味になり、1ビット右シフトすることは、2分の1 したことと同じ意味になるためです。

#include <iostream>

int main()
{
    auto value = 200u;

    std::cout << (value << 1) << "\n"
              << (value >> 1) << "\n";
}

実行結果:

400
100

かつては、*/ による乗算、除算よりも、実行速度が速くなる場合があることから、このような書き方が好まれた時代がありました。しかし現代では、仮に本当に効果があるのだとしても、コンパイラによる自動的な最適化に任せるのが一般的になっています。前述しているとおり、シフト演算には未定義の動作や処理系定義の動作が多くあるため、乗算や除算は素直に */ を使うのが安全です。

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次の変数は、どのような値で初期化されますか?

unsigned int value {~0u};

解答・解説

問題2 (確認★)

次のコードはどのような意味を持つでしょう?

auto value = 1u;
value ^= 1;
value ^= 1;

解答・解説

問題3 (基本★★)

次のコードはどのような演算を行っているか説明してください。

result = (a | b) & ~(a & b);

解答・解説

問題4 (応用★★)

ある unsigned int型の整数に、1 が立ったビットが何ビットあるか調べる関数を作成してください。

解答・解説

問題5 (応用★★★)

コンピュータで扱う色を、光の三原色(赤・緑・青)の強さを数ビットずつ使って表現することがあります(RGB形式)。たとえば、それぞれに 8ビットを使い、赤・緑・青の順に並べる形式では、真っ赤な色を 11111111 00000000 00000000 のように表せます(16進数で 0xff0000)。これは全部で 24ビットですが、あえて上位に 8ビットの未使用領域を加えた 32ビットの整数型(ここでは Color_t型とする)で扱うものとして、以下のような関数を作成してください。

  1. 赤・緑・青のそれぞれの強さを指定して、Color_t型の変数の値を変更する関数
  2. Color_t型の値を渡すと、赤・緑・青それぞれの強さに分解する関数

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る