ビット演算

ビット演算は照明スイッチの列の操作のようなものです——各照明が1ビットに対応し、他に影響を与えずに個別にオン、オフ、反転できます。

2進数の復習

コンピュータは内部ですべてのデータを2進数で表現します。1バイト=8ビット、各ビットは0または1です。

10進数 42  =  2進数 00101010
10進数 255 =  2進数 11111111

符号付き整数は2の補数表現を使います:最上位ビットが符号ビットで、0が正、1が負です。

10進数  5  =  00000101
10進数 -5  =  11111011  (2の補数)

6つのビット演算子

演算子 名称 規則
& ビットAND 両方のビットが1の場合のみ結果が1
| ビットOR 少なくとも1つのビットが1なら結果が1
^ ビットXOR 2つのビットが異なる場合に結果が1
~ ビットNOT 0が1に、1が0になる
<< 左シフト ビットを左にずらし、低位は0で埋める
>> 右シフト ビットを右にずらし、高位は符号ビット(符号付き)または0(符号なし)で埋める

ビットAND(&)

  1010  (10)
& 1100  (12)
------
  1000  (8)

用途:特定のビットのクリア、特定のビットが1かどうかの確認。

ビットOR(|)

  1010  (10)
| 1100  (12)
------
  1110  (14)

用途:特定のビットを1にセット。

ビットXOR(^)

  1010  (10)
^ 1100  (12)
------
  0110  (6)

性質:a ^ a = 0a ^ 0 = a。XORは可逆であり、同じ値で2回XORすると元に戻ります。

ビットNOT(~)

~ 00101010  (42)
= 11010101  (-43, 2の補数)

符号なしの場合:~0 = 255(8ビット)、~0 = 0xFFFFFFFF(32ビット)。

左シフト(<<)

5 << 2
= 00000101 << 2
= 00010100
= 20

nビット左シフトは2^n乗算と等価です。5 << 2 = 5 * 4 = 20

右シフト(>>)

20 >> 2
= 00010100 >> 2
= 00000101
= 5

nビット右シフトは2^n除算(ゼロ方向への丸め)と等価です。

⚠️ 注意: 符号付き負数の右シフトは高位を1で埋める(算術シフト)ため、予期しない結果になる場合があります。符号なしの値のみシフトすることを推奨します。

4つの基本ビット操作

ビットをセット(1にする)

C
flags |= (1 << n);

n番目のビットを1にし、他のすべてのビットはそのままにします。

ビットをクリア(0にする)

C
flags &= ~(1 << n);

~(1 << n)はn番目の位置だけが0で残りがすべて1のマスクを生成します。ANDを取ることでそのビットだけをクリアします。

ビットをトグル(反転)

C
flags ^= (1 << n);

XOR演算:0^1=1、1^1=0で、トグルが完璧に実装されます。

ビットをチェック

C
if (flags & (1 << n)) {
}

n番目のビットが1なら結果は非ゼロ、0なら結果はゼロです。

C
#include <stdio.h>

void print_bits(unsigned char val) {
    for (int i = 7; i >= 0; i--) {
        printf("%d", (val >> i) & 1);
    }
    printf("\n");
}

int main(void) {
    unsigned char flags = 0;

    flags |= (1 << 3);
    printf("ビット3をセット: ");
    print_bits(flags);

    flags |= (1 << 5);
    printf("ビット5をセット: ");
    print_bits(flags);

    flags &= ~(1 << 3);
    printf("ビット3をクリア: ");
    print_bits(flags);

    flags ^= (1 << 5);
    printf("ビット5をトグル: ");
    print_bits(flags);

    flags ^= (1 << 7);
    printf("ビット7をトグル: ");
    print_bits(flags);

    if (flags & (1 << 7)) {
        printf("ビット7は1です\n");
    }

    return 0;
}
▶ 試してみよう
TEXT
ビット3をセット: 00001000
ビット5をセット: 00101000
ビット3をクリア: 00100000
ビット5をトグル: 00000000
ビット7をトグル: 10000000
ビット7は1です

マスク技法

マスクとは、データ内の特定のビットフィールドを抽出または変更するために使う、あらかじめ定義されたビットの組み合わせです。

下位ビットの抽出

C
unsigned int val = 0xABCD;
unsigned int low_byte = val & 0xFF;

0xFFは最下位8ビットだけを保持するマスクです。

上位ビットの抽出

C
unsigned int high_byte = (val >> 8) & 0xFF;

まず8ビット右シフトし、マスクで下位8ビットを取得します。

値の結合

C
unsigned int combined = (high << 8) | low;

2バイトを16ビット値に結合します。

RGB色値の抽出。24ビット色値は赤、緑、青にそれぞれ8ビットずつ割り当てられています:

C
#include <stdio.h>

int main(void) {
    unsigned int color = 0xFF6633;

    unsigned char r = (color >> 16) & 0xFF;
    unsigned char g = (color >> 8) & 0xFF;
    unsigned char b = color & 0xFF;

    printf("色 #FF6633:\n");
    printf("  赤: %d\n", r);
    printf("  緑: %d\n", g);
    printf("  青: %d\n", b);

    unsigned int new_color = 0x00;
    new_color |= ((r / 2) << 16);
    new_color |= ((g / 2) << 8);
    new_color |= (b / 2);
    printf("暗くした色: #%06X\n", new_color);

    return 0;
}
▶ 試してみよう
TEXT
色 #FF6633:
  赤: 255
  緑: 102
  青: 51
暗くした色: #7F3319
💡 ヒント: RGB色の抽出はビット演算の古典的な応用です。Web開発の#RRGGBB形式は本質的に24ビット整数であり、赤がビット16〜23、緑がビット8〜15、青がビット0〜7です。

パーミッションフラグ

Linuxのファイルパーミッションはビット演算の古典的な応用です。9つのパーミッションビットが所有者/グループ/その他の読み取り/書き込み/実行を表します:

rwxr-xr-x = 111101101 = 0755
rw-r--r-- = 110100100 = 0644
C
#include <stdio.h>

#define READ    (1 << 2)
#define WRITE   (1 << 1)
#define EXECUTE (1 << 0)

void show_permission(unsigned char perm) {
    printf("%c", (perm & READ) ? 'r' : '-');
    printf("%c", (perm & WRITE) ? 'w' : '-');
    printf("%c", (perm & EXECUTE) ? 'x' : '-');
}

int main(void) {
    unsigned char owner  = READ | WRITE | EXECUTE;
    unsigned char group  = READ | EXECUTE;
    unsigned char other  = READ | EXECUTE;

    printf("パーミッション: ");
    show_permission(owner);
    show_permission(group);
    show_permission(other);
    printf("\n");

    owner &= ~WRITE;
    printf("書き込みを削除後: ");
    show_permission(owner);
    show_permission(group);
    show_permission(other);
    printf("\n");

    return 0;
}
TEXT
パーミッション: rwxr-xr-x
書き込みを削除後: r-xr-xr-x

実用的なビットテクニック

一時変数なしで2つの変数を交換

C
a ^= b;
b ^= a;
a ^= b;

原理:XORの自己逆性。ただし、可読性が下がるため、実際の開発では推奨されません。

奇数・偶数の判定

C
if (n & 1) {
}

最下位ビットが1なら奇数です。n % 2より高速です。

2の累乗での乗算・除算

C
n << 1
n << 2
n >> 1

コンパイラは通常n * 2をシフトに最適化しますが、シフトは正の整数に対してのみ安全です。

2の累乗の計算

C
unsigned int pow2 = 1u << n;

1u << 0 = 11u << 1 = 21u << 8 = 256……シフトはpow(2, n)よりはるかに高速です。

2の累乗へのアライメント

C
unsigned int aligned = (value + mask) & ~mask;

例えば、4バイト境界にアライメント:(n + 3) & ~3

C
#include <stdio.h>

int main(void) {
    int values[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    for (int i = 0; i < 10; i++) {
        int aligned = (values[i] + 3) & ~3;
        printf("%d -> %d\n", values[i], aligned);
    }
    return 0;
}
TEXT
0 -> 0
1 -> 4
2 -> 4
3 -> 4
4 -> 4
5 -> 8
6 -> 8
7 -> 8
8 -> 8
9 -> 12

❓ よくある質問

Q なぜシフト演算は乗算や除算より高速なのですか?
A シフトは1クロックサイクルのCPU命令ですが、乗算や除算は複数のクロックサイクルが必要です。ただし、現代のコンパイラは2の累乗による乗算を自動的にシフトに最適化するため、手動で書く必要はありません。
Q ビット数を超えてシフトするとどうなりますか? *A:未定義動作です!32ビットのintを32ビット以上シフトすることは許可されていません。シフト量が0からsizeof(type)8-1の間であることを確認する必要があります。

Q:ビット演算は浮動小数点数にも使えますか?

A 使えません。ビット演算子は整数型にのみ動作します。浮動小数点数のビットを操作するには、memcpyでバイトを整数にコピーする必要があります。
Q XOR交換の何が問題ですか?
A aとbが同じメモリ(同じ変数など)を指している場合、XOR交換は値をゼロにしてしまいます。さらに可読性が低く、コンパイラは通常の交換を同等に最適化します。

📖 まとめ

  • 6つのビット演算子:&|^~<<>>
  • セットは|=、クリアは&= ~、トグルは^=、チェックは&
  • マスクはビット演算の核となる技法で、ビットフィールドの抽出と結合に使用
  • パーミッションフラグはORで組み合わせ、ANDで確認、NOTでクリア
  • 左シフトは2倍、右シフトは1/2、アライメントは(n + mask) & ~mask

📝 練習問題

  1. 整数の2進表現における1ビットの数を返す関数を作成してください(ループで2で割るのではなく、ビット演算を使って)
  2. ビット演算を使ってRGB色の明るさを調整するプログラムを作成してください(各チャンネルに係数を掛けて再結合)
  3. 32ビット整数のビットmからビットnを抽出する関数を作成してください(m < n、ビット0から数える)
100%