Index

HOME > プログラムTOP > C++



C++の壺・ビット篇

 C++の基本テクニックのTipsです。ビットは、オンとオフを表現する単位で、コンピュータが動作する最小の単位でもあります。C++では、これを制御するプログラミングが可能です。ビット篇では、ビット演算と、ビットを利用した基本的なデータ処理の手法を扱います。
[INDEX] ビット演算 ... ビット演算子の種類と用法
ビット・フラグ ... フラグをビットの制御で表現
ビットフィールド ... ビットフィールドのつくり方

sasaraan programming

Exposition

●ビット演算

 プログラムでは、通常、1バイト単位でデータを扱いますが、ビット単位で制御することも可能です。1ビットは 0 か 1 のいずれかの値を取ります(2進法)。これが八つ並ぶと1バイトとなります。16進法は、4ビット単位の値の組み合わせからなり、例えば1バイトは、上位4ビットと下位4ビットとに分かれます。
 いずれもメモリと値の関係を視覚的に捉えやすい表現方法といえます。ただし、プログラムの記述で2進数の表現手段は用意されていないので、通例は 16進数でビット演算をすることになります。

1バイトの表現方法の比較

2進法 00010001 10000001 11111111
16進法 0x11 0x81 0xFF
10進法17 129255
 ビット単位での演算には、通常の演算子のほか、専用のビット演算子が利用できます。
ビット演算子and : &or : |xor : ^not : ~
内 容 両方共に1なら1
それ以外は 0
片方でも1なら1
それ以外は 0
両方異なれば 1
両方同じなら 0
0なら1 に反転
1なら0 に反転
演算例
(2進法)
 1100
&)1010
 1000
 1100
|)1010
 1110
 1100
^)1010
 0110

~1001
=0110
イメージ
論理積
論理和
排他的論理和
 ビット演算子には、ビットを右または左にシフトさせる機能を持つものもあります。これは指定した方向に指定したビット分だけシフトさせるものです。左に1ビットシフトすると、2倍 の値に、右に1ビットシフトすると、1/2 の値になります。
 また、シフトによって、はみ出した部分は破棄され、新たに割り当てられたビットには 0 が入ります。ただし、符号あり整数 (signed) の右シフトに限り、値を 2倍 または 1/2 にする整合性を保つため、左側は、最上位ビット(符号ビット)と同じ値(+の場合は 0, −の場合は 1)で埋められます。
 ただし、符合に関わらず 0 を入れるコンパイラもあるので確認が必要です。このため、ビットシフトは unsigned 整数のみを対象にすることが多いようです。
ビットシフト左シフト : <<右シフト : >>
演算例2進法 00000001 << 4 = 00010000 10000000 >> 4 = 00001000
16進法 0x01 << 4 = 0x100x80 >> 4 = 0x08
演算例
unsigned char
2進法 11111111 << 4 = 11110000 11111111 >> 4 = 00001111
16進法 0xFF << 4 = 0xF00xFF >> 4 = 0x0F
演算例
signed char
2進法 00001111 << 2 = 00111100 11110000 >> 2 = 11111100
16進法 0x0F << 2 = 0x3C0xF0 >> 2 = 0xFC
複合シフト2進法 11111111 << 3 >> 2 = 00111110
16進法 0xFF << 3 >> 2 = 0x3E
(* 薄字は捨てられた部分、太字は新たに埋められた部分)
【参考】 符号つき(signed)整数は、下表のように、2進数や16進数では符号が付きません。一方、符号つき整数では、2進数の最上位ビット(一番左側のビット)で +/− の判断が付くことが分かります。
[char型の値]2進数16進数unsignedsigned
000000000x0000
000000010x0111
011111110x7F127127
100000000x80128-128
100000010x81129-127
111111110xFF255-1

●ビット・フラグ

 フラグは、オン (1) とオフ (0) を切り替えて使用するデータの呼称です。C++ では、bool 型 (true / false) がフラグの表現に適したデータ型です。ただし、bool 型は、通常は int 型と同じバイト数 (32ビット環境時で 4バイト) を消費してしまいます。メモリへの負担を軽くしたい時は、以下のようなビット・フラグを使用します。
// ビット・フラグの例 1:三原色の組み合わせ
#include <stdio.h>     // printf
#include <conio.h>    // getch

#define RED       0x01        // 00000001 : 1
#define GREEN   0x02        // 00000010 : 2
#define BLUE     0x04        // 00000100 : 4
#define WHITE (RED | GREEN | BLUE)     // 00000111 : 7

// 色名の出力
void get_color_name(unsigned int c)
{
    // 下位3ビットのみを抽出
    unsigned char flag = (c & RED) | (c & GREEN) | (c & BLUE);

    // 分岐処理
    switch (flag) 
    {
    case RED:               printf("Red\n");     break;
    case GREEN:           printf("Green\n");  break;
    case BLUE:             printf("Blue\n");    break;
    case RED | GREEN:  printf("Yellow\n");  break;
    case RED | BLUE:     printf("Purple\n"); break;
    case GREEN | BLUE: printf("Aqua\n");   break;
    case WHITE:            printf("White\n");  break;
    default:                   printf("Black\n");
    }
}

// main 関数
void main() 
{
    unsigned char color;

    // GREEN をオン
    color = GREEN;              // = 00000010
    get_color_name(color);        // "Green"

    // RED と BLUE をオン ( | )
    color |= RED | BLUE;      // 00000010 | 00000101 = 00000111
    get_color_name(color);        // "White"

    // BLUE をオフ ( ~ と & )         00000111 & ~00000100
    color &= ~BLUE;             // 00000111 & 11111011 = 00000011
    get_color_name(color);        // "Yellow"

    // RED がオンかどうかを調べる
    if ((color & RED) == RED)  printf("赤が含まれています\n");
    else                     printf("赤は含まれていません\n");

    // WHITE がオンかどうかを調べる
    if ((color & WHITE) == WHITE)  printf("白です\n");
    else                  printf("白ではありません\n");
    
    if (getch()) return;
} 
 上記中、#define では、10進数を使用しても同じ機能となります。各データの出し入れはビット演算子を用いて行います。1 にしたいときは or ( | )、0 にしたい時は not ( ~ ) 〜 and ( & ) で設定することができます。(0 にする際、オンになっていることが分かっていれば xor ( ^ ) でも可)
 また、あるフラグがオンかどうかを知るには、上例のように、(color & RED) が、RED と同じになるかを調べます。この時、フラグがひとつのビットのオン/オフしか制御していないことが明らかなら、下例の get_days 関数の (n & (1<<i)) のように、値が 0 かどうかを調べるだけで判断できます。
 一方、下記の例は、ビットシフトを利用してフラグを表現したケースです。ビットシフトの実行は、ここではマクロで定義しています。この例では、通常なら bool 型の配列を32個用意するところを、unsigned int 1個分のメモリ (32ビット) で済むことになります。
// ビット・フラグの例 2:日付フラグ
#include <stdio.h>     // printf
#include <conio.h>    // getch

// マクロ定義 (FLAG_DAY(2) は ...0100、FLAG_DAY(3)は ...1000 を表す)
#define FLAG_DAY(N)    (1 << (N))  // N の分だけ 1 を左シフト

// 日付の出力
void get_days(unsigned int n) {
    int i ;
    for (i=0; i<32; i++) {
        if (n & ( 1<< i )) printf("%d日, ", i);  // 1 になっているビットを取り出す
    }
    printf("\n");
}

// main 関数
void main() 
{
    unsigned int days;
 
    // 10日と15日をオン : 00000000 00000000 10000100 00000000
    days = FLAG_DAY(10) | FLAG_DAY(15);
    get_days(days);        // 10日, 15日, 

    // 20日をオン : 00000000 00010000 10000100 00000000
    days |= FLAG_DAY(20);
    get_days(days);        // 10日, 15日, 20日, 

    // 15日をオフ : 00000000 00010000 00000100 00000000
    days &= ~FLAG_DAY(15);
    get_days(days);        // 10日, 20日, 
 
    if (getch()) return;
} 

●ビットフィールド

 構造体の要素には、割り当てるビット数を指定することができます。指定するには、要素の末尾に ":ビット幅" の記述を付け加えます。データ型は unsigned int でも消費は 指定したビット数のみとなります。下記は、ビット・フラグをビットフィールド内で適用したものです。
// ビットフィールドの例 1:フラグ
#include <stdio.h>     // printf
#include <conio.h>    // getch
#include <string.h>    // memset

// primcolor 構造体
struct primcolor {
    unsigned int red :1;        // 1 ビット割り当て
    unsigned int green :1;     // 1 ビット割り当て
    unsigned int blue :1;       // 1 ビット割り当て
};

// 色名の出力
void get_color_name(primcolor *pc)
{
    if (pc->red && pc->green && pc->blue) printf("White\n");
    else if (pc->red && pc->green)   printf("Yellow\n");
    else if (pc->red && pc->blue)     printf("Purple\n");
    else if (pc->green && pc->blue)  printf("Aqua\n");
    else if (pc->red)                      printf("Red\n");
    else if (pc->green)                   printf("Green\n");
    else if (pc->blue)                     printf("Blue\n");
    else                                       printf("Black\n");
}

// main 関数
void main() 
{
    primcolor pc;

    memset(&pc, '0', sizeof(primcolor));  // 0 で初期化
    get_color_name(&pc);                      // "Black"

    pc.green = 1;                              // green をオン
    get_color_name(&pc);                      // "Green"

    pc.red = 1;                                  // red をオン
    get_color_name(&pc);                      // "Yellow"

    pc.blue = 1;                                 // blue をオン
    get_color_name(&pc);                       // "White"

    pc.green = 0;                              // green をオフ
    get_color_name(&pc);                      // "Purple"

    if (getch()) return;
} 
 下記は、ビットフィールドで数値を扱う例です。数値を扱う際も、効率的にメモリを使用することができます。宣言時にビット幅を付け加えているだけで、実際の使用は普通の構造体と変わりません。
// ビットフィールドの例 2:日付
#include <stdio.h>     // printf
#include <conio.h>    // getch
#include <string.h>    // memset

// bitdate 構造体
struct bitdate {
    unsigned int month : 4;  // 4 ビット割り当て(使用可能範囲 2^4 : 0...15)
    unsigned int day : 5;      // 5 ビット割り当て(使用可能範囲 2^5 : 0...31)
    unsigned int week : 3;    // 3 ビット割り当て(使用可能範囲 2^3 : 0...7)
};

// 日付の出力
void get_date(bitdate *bd) 
{
    char *wd[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
    printf("%d/%d(%s)", bd->month, bd->day, wd[bd->week]);
}

// main 関数
void main() 
{
    bitdate bd;

    memset(&bd, '0', sizeof(bitdate));  // 0 で初期化
    bd.month = 9;                         // 月を設定
    bd.day = 23;                            // 日を設定
    bd.week = 5;                           // 曜日を設定
    get_date(&bd);                            // 出力 "9/23(Fri)"

    if (getch()) return;
} 
 ビットフィールドでは、ビット数 0 の要素をはさむことにより、次の要素を次のバイトの頭に移動させことが可能です(ビット・パディング)。また、ビットフィールドではないデータとの混在も可能です。
struct bitdate {
    unsigned int month : 4;      // 1バイト目 (4ビット消費)
    unsigned int      : 0;      // ビット・パディング (1バイト目の残り4ビットを飛ばす)
    unsigned int day     : 5;      // 2バイト目の頭へ (5ビット消費)
    unsigned int      : 0;      // ビット・パディング (2バイト目の残り3ビットを飛ばす)
    unsigned int week   : 3;      // 3バイト目の頭へ (3ビット消費)
    char memo[1024];             // 通常の 文字列
};	

www.sasaraan.net

(c) morijoh