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

トップページC言語編

このページの概要

以下は目次です。


ビットフィールド

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

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

ただし、ビットフィールドは、処理系に依存する部分が非常に多い機能です。移植性を高くすることが難しいので、複数の処理系に対応させたいときは十分に検証するようにしてください。

ビットフィールドを使うには、構造体や共用体の定義の際に、メンバに割り当てるビット数を併記するように記述します。

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

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

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

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

ビット数の指定を行う場合、そのメンバの型は、int型、signed int型、unsigned int型、_Bool型、あるいは処理系定義の型のいずれかでなければなりません。大きさはビット数の指定で決めるので、型の本来の大きさは無関係です。

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

_Bool型を指定したからといって、そのメンバが必ず 0 (偽)、1 (真) のいずれかにしかならないわけではなく、設定した値をそのまま持ちます。たとえば、_Bool型のメンバの「ビット数」を 3 にして、そのメンバの値に 4 を設定したら、そのメンバの値は 4 のままです。しかし、Visual Studio 2017 では 1 に変換されるようですし、clang 5.0.0 では _Bool型に 2ビット以上を指定できないというコンパイルエラーになります。_Bool型を指定しつつ、「ビット数」を 2以上にすることにあまり意味はないと思いますし、unsigned int を使ったほうがいいでしょう。

ビットフィールドの大きさはビット単位ですが、構造体や共用体全体の大きさは、いつもバイト単位です。これは、構造体や共用体のオブジェクトのメモリアドレスを表現できなければならないためです(そうでないと、その構造体や共用体を指すポインタを表現できません)。

実際にどれだけの大きさになるかは処理系が決定することになっており、具体的な大きさは分かりません。なお、この大きさを記憶域単位 (addressable storage unit) と呼びます。

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

#include <stdio.h>

struct Data {
    signed int a : 5;
};

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

実行結果:

4

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

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

あるビットフィールド(a) が、記憶域単位を使いきらなかった場合、次のビットフィールド(b) がその残りの部分を使おうとします。

このとき、b が必要としているビット数が、a が余らせたビットに納まりきらないときは、入る分だけを入れて、入りきらなかった分を次の記憶域単位へ入れるか(つまり、2つの記憶域単位をまたがるか)、諦めて b の全体を次の記憶域単位へ入れるかは処理系定義です。

このようなルールを把握したうえで、ビットフィールドを並べる順番を工夫しましょう。工夫せずに使うと、トータルのメモリ使用量が減らないかもしれません。たとえば、次のサンプルプログラムを見てください(これは、Visual Studio、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("%zu\n", sizeof(struct Data));
}

実行結果:

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("%zu\n", sizeof(struct Data));
}

実行結果:

8

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

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

つまり、以下のような状態です。

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

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

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

ビットフィールドのメンバ名は省略できます。この場合、名前のないビットフィールド (unnamed bit-field) となります。

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

たとえば、メンバが 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("%zu\n", sizeof(struct Data));
}

実行結果:

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);
}

実行結果:

15 7 100

ビットフィールドの価値として分かりやすいのは、極限まで小さな領域にデータを詰め込める点です。しかし、中途半端なビットにある置かれている値をアクセスするには、処理時間が余分に必要であることが多いです。これは、いったんバイト単位でアクセスしてから、必要なビットを切り出してくるような処理が必要になるためです。

メモリが非常に少ない環境では、この用途でビットフィールドを使うことに価値があるかもしれません。しかし、メモリが十分に豊富な環境では、節約の意味がほとんど無いうえに、処理速度も低下する恐れがあるため、ほとんど価値がありません。

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


練習問題

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


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



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

次の章へ (第57章 最適化に関する機能)

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

Programming Place Plus のトップページへ



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