初級C言語Q&A(11)

初出: C MAGAZINE 1996年4月号
Updated: 1996-07-31

[1つ前] [1つ後] [質問一覧] [記事一覧] [ホームページ]


構造体

 構造体は、低級に近い言語と言われるC言語の中では高級な仕様に属する機能と いえます。構造体の概念はC++になるとclassに発展し、さらに複雑なデータ構造 を扱えるようになっています。プログラムを書くのに必須ではありませんが、全 体の見通しをよくするには劇的な効果が得られるので、ぜひマスターして欲しい と思います。


構造体

Q 【構造体】

 構造体とは何か。どのような時に使うのか。

 C言語では、特に何も宣言しなくても標準で使うことのできる型がいくつかあり ます。 intcharfloatdouble 、などです。これらの型は、大きさが処理系に依存する可能性はありますが、お おむねの用途に対応してあらかじめ決められているわけです。

 ところで、さまざまなデータを扱う場合に、これらの型を組み合わせて意味を 持たせたいことがあります。例えば、NIFTY-Serveのフォーラムに発言された内容 を管理することを考えます。ログファイルの中から発言を切り出し、各発言に対 して、発言番号、発言日時、発言者のID、コメント元となる発言番号、という情 報を抜き出すことを考えます。抜き出す処理自体は今回は本質的でないので省略 しますが、抜き出す処理の後、得られた情報に対して、ファイルに情報のみを書 き込む処理を呼び出すことにします。

register_message(long num, long date, char *id, long ref)
{
    /* ファイルに書き込む */
}
num は発言番号、 date は発言日、 id は発言者IDの入った文字例、 ref はコメント元がある場合のコメント元発言番号です。

 この処理は一見明快ですが、呼び出す時にどの引数をどの順番で与えるかとい う規則に明白な根拠がありません。従って、うっかり次のように呼び出してしま っても、その時点では気が付かないかもしれません。

    /* ログから情報を切り出す .. */
    register_message(date, num, id, ref);
 これらのデータは一つの発言から得られた情報として互いに関連があるはずで す。それをプログラム中で表現しなかったことが、失敗の原因となっています。 C言語では、このような関連性のある情報は、一つの型の中にまとめて押し込むこ とができます。これが構造体と呼ばれているものです。

 具体的には、次のようにデータをまとめます。

struct msg_info {
    long num;
    long date;
    char *id;
    long ref;
};
 これで、 struct msg_info という構造体は、その中に long の型を持つ num という名前の要素と、 long の型を持つ date という名前の要素と、 char へのポインタ型を持つ id という名前の要素と、 long の型を持つ ref という名前の要素をその中に含んでいる、という宣言になります。

 これで、構造体を使ってユーザ定義の型を宣言できました。次に、その宣言を 使って、変数を定義してやりましょう。

struct msg_info mi;
 これで変数が定義できました。実際は mi などという省略した変数名はあまりよくありません。もっと意味のある名前を付 けてください。それはさておき、 mi は次のような四つの部分を持った変数です。
    +-----------------------+
    |     |     |     |     | num
    +-----------------------+
    |     |     |     |     | date
    +-----------------------+
    |     |     |     |     | id
    +-----------------------+
    |     |     |     |     | ref
    +-----------------------+
 id は処理系によっては2バイトかもしれません。

 一つの変数の中にこれらの四つの部分がありますから、それぞれの部分に対し てデータを読み書きする仕組みが必要です。このためには、「 . 」という演算子を使います。

    mi.num = num;
    mi.date = date;
    mi.id = id;
    mi.ref = ref;
 このように「 . 」を付けた左に変数名、右に、その変数の中で指定したい部分を書きます。上の 例は、 mi という変数の numdateidref の部分に、目的の情報を書き込んでいることになります。このようにして構造体 に値をセットしたら、その構造体を使って関数を呼び出すには、もはや各要素を 気にする必要はありません。
    register_message(mi);
 このように、 mi というひとかたまりのデータを一度で指定することが可能になりました。受取側 の関数は、次のようにして、それぞれの内容に対して個別に処理することができ ます。
register_message(struct msg_info mi)
{
    write_long(mi.num);
    write_long(mi.date);
    write_str(mi.id);
    write_long(mi.ref);
}

Q 【可変長の構造体】

 次のような構造体を見かけた。 handle の部分には、実際は長い文字列を格納できるようである。これは正しい書き方か。
struct nameholder {
    int id;
    char handle[1];
};

 この構造体は、おそらく、次のような使い方をするはずです。
struct nameholder *alloc_nameholder(int id, char *handle)
{
    struct nameholder *np;

    np = malloc(sizeof(struct nameholder) + strlen(s));
    if (np != NULL) {
        np->id = id;
        strcpy(np->handle, handle);
    }
    return np;
}
sizeof(nameholder) で得られた大きさの中には、最低0バイトの長さの文字列(すなわち、いきなり NUL が現れるような文字列)を保存することしかできません。かといって、
struct nameholder {
    int id;
    char handle[80];
};
 のように宣言すると、
    np = malloc(sizeof(struct nameholder));
 で得られる領域は、文字列の長さにかかわらず、 strlen() の戻す値が79であるような文字列を格納できる固定長の領域です。文字列の長さ が数バイトしかない場合には、これはメモリの無駄使いになってしまうでしょう。

 そこで、 nameholder としては、とりあえずサイズを1で宣言しておき、実際にメモリを獲得する時には 文字列を入れられる十分なサイズを得るという発想になるのです。

 実際、この発想は非常にポピュラーで、このようなソースは探せば随所に出て くるはずです。

 ところで、C言語の仕様という見地から考えてみると、 nameholder という型は、あくまで intchar[1] の組み合わせであるため、 handle の添字が1を超えてしまうと厳密には不都合を生じます。つまり、配列の添字とし て許されているのは、その配列の大きさを n とした場合に、 n-1 番目までの要素の中身か、あるいは比較のために配列の要素を指しているポイン タの値を使う場合に、 n 番目に要素があると仮定した場合の、それを指すポインタです。言葉で書くとや やこしいですが、つまり、

    int a[10];
 と宣言した場合に、仕様で許されるのは、 a[0]a[9]aa+1a+9a+10 へのアクセスである、ということです。従って、 char handle[1]とした場合に、 handle[0]から後ろをアクセスするのは仕様に反するという解釈を せざるを得ません。

 確実に仕様に適合するコードを書くのは難しくありません。

struct nameholder {
    int id;
    char *handle;
};

    np = malloc(sizeof(struct nameholder));
    if (np != NULL) {
        np->handle = malloc(strlen(s) + 1);
        if (np->handle != NULL) {
            strcpy(np->handle, s);
        }
    }
 このコードは仕様上の問題がない反面、 malloc を二回呼ばないといけないというコストがかかります。

(参考: c.l.c FAQ 2.6)


Q 【可変長の構造体(2)】

 では、これはどうだろうか。
struct nameholder {
    struct nameholder *next;
    char name[256];
};

    np = malloc(sizeof(nameholder) - (255 - strlen(s)));

 つまり、構造体の宣言の方を十分な大きさにしておき、実際にメモリを確保す る時には少し詰めたコンパクトな領域にとどめる、という発想です。どちらかと いえば、この方が仕様には違反しないという考え方もあるようですが、かなりト リッキーであることには変わりありません。

Q 【構造体のサイズ】

 次のような構造体を宣言した。
struct foo {
    char c1;
    char c2;
};
この構造を持つ構造体xを定義して sizeof(x) を求めたら8になった。なぜ2でないのか?

 構造体の要素の間は、必ずしもびっしり詰まっているわけではありません。場 合によっては、間がスカスカの状態になることがあります。このようにする理由 は、環境によっては、その方が処理速度が期待できたりするからです。

 このような飛び地のことをパディングと読んでいます。

 sizeof(x) は、パティングを含めたバイト数を返しますので、2になるとは限らないというこ とになります。


Q 【構造体の比較】

 文字列は strcmp という関数で比較することができるが、構造体に対して structcmp のような関数はないのか?

 構造体における最も大きな問題として、パディングがあります。次の例を考え てみましょう。
struct foo {
    char c;
    short s;
    long l;
};
 これは、一見、次のようなデータ構造を意味しているように見えます。
    +-----+
    |     |  c
    +-----+-----+
    |     |     |  s
    +-----------+-----------+
    |     |     |     |     | l
    +-----------------------+
 つまり、 char c; を格納するための1バイトと、 short s を格納するための2バイ トと、 long l を格納するための4バイト、合計7バイトの大きさの構造体です。

 ところが、ある種のCPUにおいては、データのアクセスが2バイト単位とか、4バ イト単位の境界で行う必要があったり、あるいはその場合が最も効率よくデータ を処理できるように設計されていることがあります。すると、次のようなデータ 構造にすることによって、実行速度が速くなるという効果が得られるわけです。

    +-----------+-----------+
    |     |  *  |  *  |  *  | c
    +-----------------------+
    |     |     |  *  |  *  | s
    +-----------------------+
    |     |     |     |     | l
    +-----------------------+

    (*)は使わない
 さて、このような飛び地がなければ、構造体の比較は簡単です。
    int differ;
    struct foo s1;
    struct foo s2;

    /* s1, s2 に値をセットする */
    ...

    differ = memcmp((char *) &s1, (char *) &s2, sizeof(struct foo));
つまり、ぴったりとデータが詰まっていれば、 memcmp でブロック比較すれば、値が同一であるかどうかを知ることができます。しかし、 この方法は、飛び地があると失敗するかもしれません。飛び地にどんな値が入っ ているかは分からないからです。

 すると、この場合確実に内容を比較するためには、次のように処理せざるを得 ません。

    differ = (s1.c != s2.c) || (s1.s != s2.s) || (s1.l != s2.l);
 この比較方法は、 struct foo のような構造にのみ適用できるのであって、他のデータ構造に対するには、その 都度データ構造にマッチした比較の式を書いてやらなければなりません。従って、 任意の構造体を簡単に比較することは困難だというのが一般論です。

(参考: c.l.c FAQ 2.8)


Q 【構造体のセーブ、ロード】

 構造体の内容をファイルに書いたり、ファイルから読むにはどうすればよいか。

freadfwrite を使うのが、最も安易な方法です。
    fread(&s1, sizeof(struct foo), 1, fp_read);   /* 読む */
    fwrite(&s1, sizeof(struct foo), 1, fp_write); /* 書く */
 しかし、この方法は二つの問題を引き起こします。まず、エンディアンの異な るシステム間でデータをやりとりしようとする場合に、破綻するおそれがありま す。さらに、エンディアンが同じシステムであっても、アライメントの問題が発 生すると同様の問題が発生します。

 完璧なコードにするには、やはり、要素を一つずつ処理するしかありません。

/* 構造体fooをファイルから読み込む */
int read_foo(struct foo *s1)
{
    i = read_char(&(s1->c));
    if (i < 1)
        return i;
    i = read_short(&(s1->s));
    if (i < 1)
        return i;
    i = read_long(&(s1->l));
    return i;
}

/* 1バイト読む。fp_readは事前にセットされている想定 */
read_char(char *c)
{
    int i;

    i = getc(fp_read);
    if (i == EOF)
        return 0;
    *c = (char) i;
    return 1;
}

/* 2バイト読む。fp_readは事前にセットされている想定 */
read_short(short *sh)
{
    int i, j;

    i = getc(fp); /* 1バイト目を読む */
    if (i == EOF)
        return 0;
    i <<= 8;

    j = getc(fp_read); /* 2バイト目を読む */
    (if j == EOF)
        return 0;
    i |= j;

    *sh = (short) i;
    return 1;
}

/* 4バイト読む。fp_readは事前にセットされている想定 */
read_long(long *l)
{
    long tmp;
    int i

    i = getc(fp_read); /* 1バイト目を読む */
    if (i == EOF)
        return 0;
    tmp = i;
    tmp <<= 8;

    i = getc(fp_read); /* 2バイト目を読む */
    (if i == EOF)
        return 0;
    tmp |= i;
    tmp <<= 8;

    i = getc(fp_read); /* 3バイト目を読む */
    (if i == EOF)
        return 0;
    tmp |= i;
    tmp <<= 8;

    i = getc(fp_read); /* 4バイト目を読む */
    (if i == EOF)
        return 0;
    tmp |= i;

    *l = tmp;
    return 1;
}
(参考: c.l.c FAQ 2.11)

Q 【自己参照構造体】

 次のように定義したいのだが、なぜうまくいかないのか。
typedef struct {
    LIST *next;
    char *body;
} LIST;

 next を宣言する時点で LIST という型が未定義であるためエラーが発生します。次のように書けば問題ありません。
typedef struct list {
    struct list *next;
    char *body;
} LIST;
 一旦宣言した後では、 LISTstruct list は全く同じ型として扱うことができま す。

Q 【相互に参照する構造体】

(内容訂正中です)

Q 【構造体を戻す関数】

 次のようなコードを含むプログラムをコンパイルしたらエラーが発生してしま う。
struct foo {
    char c;
    short s;
    long l;
};

struct foo return_foo(void)
{
    static struct foo x;

    /* xに、値を読み込む */
    return x;
}

 構造体は、関数を呼び出す時の値として、また、関数からの戻り値として用い ることができますが、一部の古いコンパイラの中には、この機能が入っていない ものがあります。このようなコンパイラを使う場合は、構造体へのポインタを使 って値をやりとりするように修正が必要です。
  ※ c.l.c FAQ : comp.lang.c FAQ list
     URL: http://www.eskimo.com/~scs/C-faq.top.html
     文中の項目番号は新しい版に対応しています。旧版とは異なります。

(C) 1996 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です。
内容は予告なく変更することがあります。