コンピューター:C言語講座:構造体・共用体・ビットフィールド


 構造体や共用体はC++などが出現するまでは私にとって他の言語に比べて最もC言語の素晴らしい物でした。とくに構造体を使わずにCADのような大規模なアプリケーションを組めといわれたら気を失ってしまいそうです。今となってはC++やJAVAなどのように更に上をいくクラスが出現してしまっていますからやや古くさいのは否定できませんが、いずれにしてもC言語を使いこなしているかどうかはこの構造体と関数とポインターを使いこなしているかで判断できるくらい重要なものではないでしょうか?

 ここでは構造体がどういうものかとか、そういった入門書に出ているような事ではなく、ビットフィールドや共用体まで含めて考えてみたいと思います。

構造体
 構造体はご存知の通り、複数のさまざまな型のデータをまとめて管理するものです。これはデータを保持しておく際にも非常に便利ですし、関数への引数としても便利に使えます。また、バイナリーデータをファイルに保存したり読み出したりする場合にも大変便利です。しかし、あんまり便利で簡単に使えるので意外と注意を怠りがちで、結構無駄なことをしている場合も見受けられます。

 まず、関数への受渡しについてですが、構造体は意外と大きなサイズになる場合が多く、構造体の実体を引数として関数に渡すのは意外と無駄な場合が多いものです。構造体を使わずに引数をバラバラに渡している時は、こんなにたくさん実体を渡したら渡すだけでも重くなりそうだ、と気づくものですが、構造体の場合、引数としては1つになるので気にせずに実体を渡していることが多いものです。当然のことながらC言語では値渡しされますので、構造体の内容がサイズ分コピーされて関数にわたります。これではプログラマーが見やすくなっただけで、構造体ならではのメリットを半分以上捨てているようなものです。
 大きい構造体で、しかも速度が重要な場合は迷わずアドレス渡しを選ぶべきです。どんなに大きな構造体でもアドレスは32bit(64bit?)です。32bitコピーするのと、構造体のサイズ分コピーするのは大変な差になる場合が多いです。アドレス渡しが無駄なのは受け取った関数側でそのまま値を書き換えながら使って、本体には影響を残さないような使い方をする場合くらいで、ほとんどの場合はアドレス渡しのほうが悪いことはありません。強いてあげればアドレスで渡したのを忘れて内容を書き換えてはいけないのに書き換えてしまうことがある点くらいでしょう。

 代入も注意が必要です。以前は構造体を直接=で代入することはできない処理系が多かったのですが、最近はほとんどの物が問題無く代入できます。しかし、一般的に代入よりメモリーコピー(memcpy(),bcopy()など)の方が高速なのです。これはアセンブルコードを見るとわかりますが、多くのCコンパイラは代入は構造体のメンバー1つ1つをコピーします。当然、メモリーコピーは何も気にせずサイズ分一度にコピーしますので、こちらのほうが高速です。これも本当に速度を気にしている場合には注意が必要です。

 また、境界合わせもとくにバイナリーファイルなどを扱う場合に注意が必要です。例えば次の構造体のサイズはいくつだと思いますか?

struct _test_{
    char  c;
    int   i;
}test;

単純にはsizeof(char)+sizeof(int)でintが32bitなら、1+4=5byteとなりそうですが、sizeof(struct _test_)は32bit系では8byteになります。Cコンパイラーによってはオプションで5byteにすることもできるものもありますが、何も指示しなければ8byteでしょう。これはパフォーマンスを向上させるのに倍精度型のload/store命令を使いたいからで、1byteづつのアクセスよりも高速なプログラムができるように境界合わせがされているのです。
 したがって、バイナリーファイルに書き込む時に

fwrite(&test,5,1,fp);

などと、自分で計算した5byteで書き込もうとするのは全くナンセンスで、

fwrite(&test,sizeof(test),1,fp);

と書く必要があります。また、処理系によって境界合わせが異なることも考えられますので、こういったバイナリーファイルを共用する際は注意が必要です。安全なのは、

struct _test_{
    char  c,dummy1,dummy2,dummy3;
    int   i;
}test;

このように、32ビット単位でもともと構造体を作っておくことですが、それでもバイナリーファイルの場合はインテル系とモトローラー系でビットの並びが違うので注意は必要です。
 また、これを知っていると、変にサイズを小さくしようと思ってchar型を使っても意外と変わらない場合が多いということに気づき、宣言の順序なども重要になって来ます。

struct _test1_{
    char  a;
    int   b;
    char  c;
}test1;

struct _test2_{
    char  a;
    char  c;
    int   b;
}test2;

この2つの構造体はサイズが違います。test1が12byteで、test2は8byteです。

共用体
 共用体は構造体に似ていますが、構造体では各メンバーはオフセットがかけられて別々のアドレスになりますが、共用体では全てアドレスが同じになります。こんなもの何に使うのだ?と考える方も多いのですが、知っているどうかで全くプログラムの出来が違って来る場合もあります。
 例えば、直線・円・円弧の3種類のデータをメモリー上に配列状に管理したいとします。とりあえず3種類の構造体を定義しますが、3つの配列でかまわないのならそれぞれを構造体の配列にします。

typedef struct{
    double sx,sy,ex,ey;
}LINE;
typedef struct{
    double cx,cy,r;
}CIRCLE;
typedef struct{
    int   direction;
    double sx,sy,ex,ey,cx,cy;
}ARC;

LINT  Line[100];
CIRCLE Circle[100];
ARC   Arc[100];

しかし、出来れば1つの配列で管理したい時にどうしますか?全てのメンバーの最小公倍数のような構造体を作っても出来そうですが、繁雑になります。ポインターの配列を作って実体を指すようにする方法もあります。これは最もメモリー効率も良さそうですが、メモリーを割り当てたり、開放したりの管理が大変そうです。こんな時に共用体が威力を発揮します。

typedef struct{
    int   type;  /* 0:line,1:circle,2:arc */
    union{
        LINE  l;
        CIRCLE c;
        ARC   a;
    }d;
}DATA;
DATA  DATA[300];

こうすればtypeを見てそれによってd.l,d.c.d.aを使い分ければ別々のデータの扱いが出来ますし、メモリーもLINE,CIRCLE,ARCの一番大きいものの大きさ分で済みます。もちろん、この方法では、1つでもとてつもなく大きい型があると無駄が多くなってしまいますが、そうでない場合には管理も簡単で、非常に便利に使えます。

ビットフィールド
 学生のころ、電算の先生から「実数(float)を2進数表示しろ」という課題が出たことがありました。私はビットフィールドを当時から知っていたので、

#include    <stdio.h>
#include    <math.h>

void main()
{
float  f;
union{
    float  f;
    struct{
        short  b16:1;
        short  b15:1;
        short  b14:1;
        short  b13:1;
        short  b12:1;
        short  b11:1;
        short  b10:1;
        short  b9:1;
        short  b8:1;
        short  b7:1;
        short  b6:1;
        short  b5:1;
        short  b4:1;
        short  b3:1;
        short  b2:1;
        short  b1:1;
    }b;
}f_b;

    while(1){
        scanf("%g",&f);
        f_b.f=f;
        printf("%d",f_b.b.b16);
        printf("%d",f_b.b.b15);
        printf("%d",f_b.b.b14);
        printf("%d",f_b.b.b13);
        printf("%d",f_b.b.b12);
        printf("%d",f_b.b.b11);
        printf("%d",f_b.b.b10);
        printf("%d",f_b.b.b9);
        printf("%d",f_b.b.b8);
        printf("%d",f_b.b.b7);
        printf("%d",f_b.b.b6);
        printf("%d",f_b.b.b5);
        printf("%d",f_b.b.b4);
        printf("%d",f_b.b.b3);
        printf("%d",f_b.b.b2);
        printf("%d\n",f_b.b.b1);
    }
}

と解答したら、先生に叱られました。どうやらfloatのデータ形式を考えて表示してほしかったらしく、私はいまだに正しい解答を知りません(というか、考えたことがありません)。
これはもちろんshortにでもmemcpy()して、シフトしながら表示しても同じ結果が出ます(こっちのほうが簡単ですが、当時私はビット演算が苦手でしたので)。
 これは極端ですが、ビットフィールドはビット演算が苦手な人がビット単位で操作しなければならない時の救世主にもなります。昔、ハードディスクが高くて滅多に使えなかった時代にはデータをファイルに格納するとなると、ビット単位でフラグや値を散りばめて書き込んでいました。そういった場合にはいちいちマスクしたりせずにダイレクトに書き込めるので便利です。やはりビット演算のほうがスマートな為か滅多に本に書かれていません。
 構造体のメンバー宣言に:1のようにビット長を指定するだけで使えます。当然のことながら、1ビットの変数に3とかを代入するのはナンセンスです。
 なお、ビットフィールドや共用体も構造体で書いたような注意が当てはまります。境界合わせも十分考えてください。

struct{
    short  a:1;
}one_bit;

このように宣言しても当然ながらone_bitのサイズは1bitにはなりません。


よろしければブログもご覧ください
ipv400285786 from 1998/3/4