ビットフィールド | Programming Place Plus C言語編 第56章

C言語編 第56章 ビットフィールド

先頭へ戻る

この章の概要

この章の概要です。


ビットフィールド

構造体や共用体のメンバが使用するメモリの大きさを、ビット単位で指定することができます。このような指定が行われたメンバを、ビットフィールドと呼びます。

普通に型を指定するだけでは、最も小さい char型を使っても、1バイトを下回ることはできませんが、ビットフィールドならば、最小で 1ビットにまで切り詰めることが可能です。

ただし、ビットフィールドは、コンパイラに依存する部分が非常に多い機能です。移植性を考えて使用することは相当に難しいので、複数のコンパイラに対応させたいときは十分に検証するようにして下さい。

以下のように、構造体や共用体の定義の際に、メンバに割り当てるビット数を併記するように記述します。

struct タグ名 {
    型 メンバ名 : ビット数;
    型 メンバ名 : ビット数;
    型 メンバ名;
      :
};

union タグ名 {
    型 メンバ名 : ビット数;
    型 メンバ名 : ビット数;
    型 メンバ名;
      :
};

「ビット数」の指定があるメンバと、指定のないメンバは混在しても構いません。

「ビット数」には、結果が 0 以上になる整数の定数式を指定します。これが、そのメンバの大きさになります。0 の場合は特別な意味を持つので、これは後で改めて説明します。また、メンバの型の本来の大きさを超えることはできません

ビット数の指定を行う場合、そのメンバの型は、int型、signed int型、unsigned int型、あるいはコンパイラが特別に許可している型のいずれかでなければなりません。大きさはビット数の指定で決めるので、型の本来の大きさは無関係です。

C99 では、_Bool型(第13章)も許されます。

int型と signed int型は、この場面に限っては異なる意味を持ちます。signed int型を指定した場合は、必ず符号付き整数となりますが、単に int型とした場合には、符号の有無はコンパイラが決めます。そのため、ビットフィールドでは、単なる int型は避けた方が無難です。

ビットフィールドはビット単位の大きさであるものの、構造体や共用体全体としての大きさは、バイト単位になります。これは、メモリアドレスを表現可能でなければならないためです(そうでないと、その構造体や共用体を指すポインタを表現できません)。

実際にどれだけの大きさになるかはコンパイラが決定することになっており、具体的な大きさは分かりません。なお、この大きさを記憶域単位と呼びます。

試しに、1バイトを下回るビットフィールドを定義して、その大きさを出力してみましょう。

#include <stdio.h>

struct Data {
    signed int a : 5;
};

int main(void)
{
    printf( "%u\n", sizeof(struct Data) );

    return 0;
}

実行結果:

4

Data構造体には、5ビットを割り当てたメンバしかないですが、VisualStudio や clang で確認すると、4 という出力が得られます。つまり、記憶域単位は 4バイトのようです。

もし、構造体全体が1つの記憶域単位で収まらないほど大きいのなら、記憶域単位の倍数の大きさが取られることになります。

あるビットフィールド(a) が、記憶域単位を使いきらなかった場合、次のビットフィールド(b) がその残りの部分を使おうとします。このとき、b が必要としているビット数が、a が余らせたビットに納まりきらないときは、入る分だけを入れて、入りきらなかった分を次の記憶域単位へ入れるか(つまり、2つの記憶域単位をまたがるか)、諦めて b の全体を次の記憶域単位へ入れるかをコンパイラが選択します。

このようなルールを把握したうえで、ビットフィールドの並べ方を工夫しましょう。工夫せずに使うと、結局のところ、トータルのメモリ使用量は減りません。例えば、次のサンプルプログラムを見て下さい(これは、VisualStudio、clang で確認しています)。

#include <stdio.h>

struct Data {
    signed int a : 15;
    signed int b : 20;
    signed int c : 15;
    signed int d : 10;
};

int main(void)
{
    printf( "%u\n", sizeof(struct Data) );

    return 0;
}

実行結果:

12

4つのビットフィールドの合計ビット数は、60ビットです。そのため、8バイト (64ビット) あれば足りるはずですが、構造体全体の大きさは 12バイト (96ビット) になっています。

この結果になるのは、この環境では、記憶域単位が 32ビットであり、前のビットフィールドが余らせた領域が不足なら使わず、次の記憶域単位を割り当てるからです。a (15ビット) が余らせた 17ビットでは、b を収めることができないため、a と b が異なる記憶域単位を使います。さらに、b (20ビット) が余らせる 12ビットには c が収まらないため、c もまた新たな記憶域単位を使ってしまいます。c (15ビット) が余らせる 17ビットに d は収められるので、c と d は同じ記憶域単位を使います。
つまり、以下のような状態です。

非効率なビットフィールドのメモリイメージ

ここでは、各ビットフィールドが、1つの記憶域単位内でメモリアドレスの下位から上位へ向かって配置されるようにイメージしています。この点に関しても処理系定義となっており、上位から下位へ向かって配置されることもあります。

ビットフィールドの並び順を組み替えてみます。

#include <stdio.h>

struct Data {
    signed int a : 15;
    signed int c : 15;
    signed int b : 20;
    signed int d : 10;
};

int main(void)
{
    printf( "%u\n", sizeof(struct Data) );

    return 0;
}

実行結果:

8

今度は 8バイトになりました。

a が余らせた 17ビットの中に c (15ビット) を収められるので、a と c が1つの記憶域単位を共有できます。また、b は新たな記憶域単位を使いますが、12ビット余らせるので、d (10ビット) を収めることができます。
つまり、以下のような状態です。

効率的なビットフィールドのメモリイメージ

繰り返しになりますが、この結果は、記憶域単位が 32ビットであり、前のビットフィールドが余らせた領域が不足なら使わず、次の記憶域単位を割り当てる環境での話です。ルールが異なる環境では結果はまるで違ったものになります。

名前のないビットフィールド

ビットフィールドのメンバ名は省略することができます。この場合、名前のないビットフィールドとなります。

名前のないビットフィールドは、名前がないので、参照することができません。利用価値がないようですが、そこに確かにビットは割り当てられるので、明示的にビット単位のパディングを入れる効果があります

例えば、メンバが 16ビットの倍数の位置にあることが求められているとすると、以下のようにパディングを入れることで対応できます。

struct Data {
    signed int a : 15;
    signed int   : 1;  /* 1ビットのパディング */
    signed int b : 12; /* 16ビット目から割り当て */
};

また、後述するように、0ビットの指定を行うために、名前のないビットフィールドが必要です。

0 ビットの指定

名前のないビットフィールドは、ビット数の指定を 0 にすることができます。これは、前のビットフィールドが使いきらなかった、記憶域単位の残りを使わないことを意味します。

ここまでに見てきたように、前のビットフィールドが余らせた記憶域単位の残りを、次のビットフィールドが使うかどうかはコンパイラに依存している部分であって制御できませんが、0ビットのビットフィールドを使うと「使わせない」という制御ができます。

#include <stdio.h>

struct Data {
    signed int a : 15;
    signed int   : 0;   /* 余った領域は使わせない */
    signed int b : 10;  /* 新しい記憶域単位を使う */
    signed int c : 20;
    signed int d : 10;
};

int main(void)
{
    printf( "%u\n", sizeof(struct Data) );

    return 0;
}

実行結果:

12

この構造体は、メモリを以下のように使うと考えられます。

ビット数が 0 のビットフィールド使用時のメモリイメージ

ビットフィールドの使い方

ビットフィールドは、メモリアドレスを取得できないことに注意して下さい。メモリアドレスはバイト単位で割り振られているものなので、中途半端なビット位置に値を格納している可能性があるビットフィールドでは、メモリアドレスを表現できないことがあるからです。

この点を除けば、構造体や共用体のビットフィールドでないメンバと変わりありません。ビットフィールドへのアクセスは、ドット演算子やアロー演算子を使って行えます。

#include <stdio.h>

struct Data {
    signed int a : 5;
    unsigned int b : 3;
    int c;
};

int main(void)
{
    struct Data data = { 15, 7, 100 };

#if 0
    int* pb = &data.b;  /* コンパイルエラー */
#endif
    int* pc = &data.c;  /* OK */

    printf( "%d %u %d\n", data.a, data.b, data.c );

    return 0;
}

実行結果:

15 7 100

ビットフィールドの価値として分かりやすいのは、極限まで小さな領域にデータを詰め込める点です。しかし、中途半端なビットにある置かれている値をアクセスするには、処理時間が余分に必要であることが多いです。これは一旦、バイト単位でアクセスしてから、必要なビットを切り出してくるような処理が必要になるためです。
メモリが非常に少ない環境では、この用途でビットフィールドを使うことに価値があるかも知れません。しかし、メモリが十分に豊富な環境では、節約の意味がほとんど無いうえに、処理速度まで低下する恐れがあるため、ほとんど価値がありません。

もう1つの利用箇所は、フォーマット(形式)が厳密に定められているようなデータを扱わなければならないときです。例えば「先頭から 4ビットがこういう意味の値、次の 6ビットでこれを表現し、次の 6ビットで・・・」といったようなデータです。ビットフィールドを使わず、ビット演算を駆使して実現することもできますが、ビットフィールドを使った方が簡単である可能性はあります。


練習問題

問題① 1ビットのビットフィールドを1つだけ持つ構造体の大きさがどれだけになるか、調べてみて下さい。


解答ページはこちら

参考リンク



更新履歴

'2018/5/22 全体的に内容を強化。

'2018/5/19 新規作成。第55章に含まれていた内容を移動してきた。



前の章へ(第55章 共用体)

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

Programming Place Plus のトップページへ


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