[←1つ前] [→1つ後] [↑質問一覧] [↑記事一覧] [ホームページ]
構造体は、低級に近い言語と言われるC言語の中では高級な仕様に属する機能と いえます。構造体の概念はC++になるとclassに発展し、さらに複雑なデータ構造 を扱えるようになっています。プログラムを書くのに必須ではありませんが、全 体の見通しをよくするには劇的な効果が得られるので、ぜひマスターして欲しい と思います。
int
、
char
、
float
、
double
、などです。これらの型は、大きさが処理系に依存する可能性はありますが、お
おむねの用途に対応してあらかじめ決められているわけです。
ところで、さまざまなデータを扱う場合に、これらの型を組み合わせて意味を 持たせたいことがあります。例えば、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
という変数の
num
、
date
、
id
、
ref
の部分に、目的の情報を書き込んでいることになります。このようにして構造体
に値をセットしたら、その構造体を使って関数を呼び出すには、もはや各要素を
気にする必要はありません。
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); }
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
という型は、あくまで
int
と
char[1]
の組み合わせであるため、
handle
の添字が1を超えてしまうと厳密には不都合を生じます。つまり、配列の添字とし
て許されているのは、その配列の大きさを
n
とした場合に、
n-1
番目までの要素の中身か、あるいは比較のために配列の要素を指しているポイン
タの値を使う場合に、
n
番目に要素があると仮定した場合の、それを指すポインタです。言葉で書くとや
やこしいですが、つまり、
int a[10];と宣言した場合に、仕様で許されるのは、
a[0]
〜
a[9]
、
a
、
a+1
〜
a+9
、
a+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)
struct nameholder { struct nameholder *next; char name[256]; }; np = malloc(sizeof(nameholder) - (255 - strlen(s)));
struct foo { char c1; char c2; };この構造を持つ構造体xを定義して
sizeof(x)
を求めたら8になった。なぜ2でないのか?
このような飛び地のことをパディングと読んでいます。
sizeof(x)
は、パティングを含めたバイト数を返しますので、2になるとは限らないというこ
とになります。
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)
fread
と
fwrite
を使うのが、最も安易な方法です。
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)
typedef struct { LIST *next; char *body; } LIST;
next
を宣言する時点で
LIST
という型が未定義であるためエラーが発生します。次のように書けば問題ありません。
typedef struct list { struct list *next; char *body; } LIST;一旦宣言した後では、
LIST
と
struct list
は全く同じ型として扱うことができま
す。
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 文中の項目番号は新しい版に対応しています。旧版とは異なります。