09 月 30 日(水)

構造体

異種・複数のデータを ひとつにまとめたものが構造体である. 構造体を有効に活用すれば, 多くの変数を必要とするような 複雑な処理少しの変数だけで簡潔に記述できるようになり, プログラム開発作業の大幅な効率化につながる.

具体的に,たとえば, ある種のゲームプログラムの開発していると想定してみよう.

このゲームに登場するキャラクタは, 様々な属性{名前,位置,姿勢,体力,所持品,...}を持つとする. それらの属性を別個の変数で表わすとしたら, 関数呼び出しの度に,一体いくつの変数を渡さなきゃならないんだ? 開発の進行にともなって, 呼び出したい関数もたくさん増えて来たし, キャラクタも多数登場させたいんだが... これは面倒くさいことになりそうだ...

属性毎に個別の変数を多数使うのではなく, キャラクタを表わす構造体の変数を1個だけ使って済ませよう. これで面倒や間違いから開放され,幸せになれる.

もちろん,タダで楽をすることはできない. たくさんの勉強が必要となるが, 一度理解してしまえば後々快適. 永く楽をするための苦労を惜しまないこと.

教科書(K&R) pp.154-165 も参考にしよう.


新しいデータ型の定義

構造体の説明の前に, 新しいデータ型を作る方法 typedef を紹介しておく.

新しい型といっても,既存の型に新しい名前を付け直すだけだが...

この具体例では int 型を利用して Price(価格)型を新たに定義している. このようにしておけば,変数 tomatopotato が 何を表わす変数だったのか?少しだけ,わかり易くなっている.

一方,int tomato と書いた場合, トマトの何だった?重さ?大きさ?と混乱してしまうかもしれない. 「3 日後の自分は他人.

なお,この考え方の度が過ぎて... 「変数名や関数名には型名も含めなきゃ不十分だ」 と主張する人達もいます. たとえば:

Price  price_tomato, price_potato;

ある時期の Windows の開発者達は, このような流儀(a.k.a. ハンガリアン方式) を採用していた.

一方,Linux の作者達は,「そんなのは無駄」と批判していた. この流儀は「頭の頭痛が痛い」みたいで 冗長だし. 「過ぎたるは及ばざるがごとし.」

どちらにせよ, わかりやすく書きやすい名前(=誤解を生まないような変数名や型名) をつけるのが良い. 具体的にどうすれば? ...プログラマのセンスが問われる問題だ.

では,typedef の実際の使い方を理解するために, List 1 を試してみよう.

List 1. typedef のテスト
#include <stdio.h>

typedef int Price;

int main()
{
	Price tomato;

	tomato = 100;		// Price 型に int 型の 100 を代入
	printf("トマトの値段=¥ %d\n", tomato);	// Price 型を整数として表示

	return (0);
}							

なお,データ型 Price の実体は int 型なので, Price 型の変数では, int 型の変数とまったく同様に, 整数値の代入や,整数としての入出力が可能である.

ところで,List 1 について, typedef がグローバルに(関数の外部に) 記述されている理由は何だろうか? ローカルに(関数の内部に)記述しても構わないが, その型はその関数内でしか使えなくなってしまう.

構造体の定義

構造体を使うには, まず,複数のデータ型を組み合わせて構造体のデータ型を定義し, それから,その型を使って構造体の変数を宣言することになる. これで,複数のデータをひとつの変数にまとめられるようになる.

以下では具体例として,野菜情報(価格,重量,生産者名,等の組み合わせ)と 複素数(実数データと虚数データの組み合わせ)を採り上げ, 構造体の定義方法を説明する.

タグを使う定義方法

構造体の基本的な定義方法がこれだ.

タグを使わない定義方法(typedef を使う方法)

typedef を利用した構造体変数の定義もよく使われる.

タグ方式でも typedef 方式でも,どちらを使っても構わない. コーディング作業でのこれら 2 つの方式の違いは, 構造体の定義時にタグか typedef のどちらを付けるのかと, 構造体変数の宣言時に struct を付けるかどうかだけ.

この授業では,主として,typedef 方式を使う. 変数宣言の際,いちいち struct を付けるのが面倒なので...

補足(上級者向け): タグの省略が不可能な場合もある. たとえば,構造体を再帰的に定義する (その構造体のメンバ変数として同じ構造体型を含める) ような場合.


注意

テストプログラムは,後々のセクションで... しばらく,ややこしい理論説明が続くが, 効率良くプログラミングするi.e. すごいプログラムを楽に作る) ために必要な知識となるハズなので, 読み飛ばさないこと.


構造体の初期化

構造体変数へデータを代入する方法を説明する.

宣言時の初期化

構造体変数も通常の変数や配列と同様に, 宣言と同時に初期化できる.

宣言時以外の初期化(初期化関数)

残念ながら,構造体変数の全メンバへの一括代入は, 宣言文以外ではできない.

同様な制限が配列の場合にもあったよね?

配列と構造体のちがい

配列は同じ型のデータ同士の集合 (例:int 型だけ10個とか)である. 一方, 構造体は異なる型のデータの集合 (例:int 型と double 型の組み合わせ等)である. 混同しないこと.

なお,構造体では,同じ型の組み合わせでも OK. しかし,配列では,異なる型の集合はありえない.

複素数の例の場合,同じ型のデータの集合 (メンバ reim も実数型)なので, 構造体ではなく,配列によって表現することも可能だ. しかし,配列ではデータをまとめてコピーするようなことはできない:

double  z1[2] = { 1.0, 2.0 };	// 配列の場合...
double  z2[2];

z2 = z1;	// 一括代入 NG.コンパイルエラー

今回の本論からは外れるが...なぜ,これが間違いなのか? 論理的に説明しておこう.(「論理的な作文」のお勉強.)

まず,この代入式では,左辺にも右辺にも配列名が指定されている. 配列名は,配列の記録場所(アドレス)を表わすものであって, 配列の内容(データ)ではない. つまり,この式は,データのコピーを意味しておらず, そもそも,処理の目的から間違っていることになる. (ちなみに,この式は,アドレスをコピーしようとしている.)

さらに,配列のデータは変数だが,配列名はアドレスの定数だ. つまり,この式は,左辺の定数を書き換えようとしており, それは明らかに無理だ. (ちなみに,右辺も同様に定数だが,それは無関係.)

以上のことから,代入式によって, 配列を一括してコピーすることは不可能である.

一方,構造体ならば,簡単にコピーできるので便利である:

Complex z1 = { 1.0, 2.0 };	// 構造体の場合...
Complex z2;

z2 = z1;	// 一括代入 OK

しかし,乱用は禁止. たった一行の代入文なんだが, データのコピーには,データ量に比例した時間がかかる. 無駄な処理をしないこと.

特に,構造体が巨大な場合やコピー回数が多い場合には, 本当にその構造体をコピーしなければならないのか? 考えなおす必要があるだろう.

構造体が複数のデータをひとまとめにしている, ということの意味を理解できただろうか?

構造体の表示

構造体のデータ内容を確認するには, 構造体の各メンバについて printf() で表示すればよいだろう. しかし,構造体の全体を直接 printf() することはできない. なぜなら,printf() の変換指定子は 組み込み型charintdouble,等) だけにしか対応していないためである:

Complex z1 = { 1.0, 2.0 };

printf("z1 = %f +j %f\n", z1.re, z1.im);	// メンバ毎の表示は可能

printf("%???\n", z1);		// 全部一辺には不可能

例:複素数計算

複素数計算プログラムの例を用意してある. 試してみよう.

複素数構造体は,たったの 2 個のメンバしかもっていないので, なぜ,わざわざ構造体を使うのか? まだ,疑問に思っているかもしれない. 構造体を使わずに,プログラムを作り変えてみればわかる:

なお,構造体さえ使えば良いってものでもない. 「うまく」使わなければ, 逆効果になる場合もある. 状況に応じて,どの技を使えば楽になるのか?よく考えよう. 「楽をするためなら,どんな苦労も惜しまない」のが良い技術者.

例:データベース

「構造体の構造体」や「構造体の配列」も定義できる. 前者の例は,K&R p.157 にある. ここでは,後者の例を挙げる.

構造体配列は表データ(table)を処理するために良く利用される. 配列要素のメンバへアクセスするには, 次のように, 「構造体変数[要素番号].メンバ」という形式を使う:

Data data[...];
int  i;
...

while (...) {
	printf(..., data[i].name);
	i++;
}

配列のついでに,構造体へのポインタについても説明しておく. ポインタによって構造体メンバにアクセスするには, 次のように, 「ポインタ->メンバ」という形式を使う:

Data *data;
...

while (...) {
	printf(..., data->name);
	data++;
}

次の動物データベースプログラムの例を試してみよう.


練習問題

  1. 複素数計算プログラム complex.c について, 積と和の両方を表示できるように改造せよ.
  2. 複素数の和を計算する関数 ComplexAdd() を追加すればよいだろう.
  3. 動物データベースプログラム dbase.c について, 種類別に検索できるように改造せよ.
  4. 構造体に分類コードのメンバ class を追加すればよいだろう. また,分類コードの値としては,たとえば, 哺乳類なら 0,鳥類なら 1,爬虫類なら 2,両生類なら 3,甲殻類なら 4, その他なら 5,のような整数値を使うことにすれば簡単.
  5. (余裕があれば)complex.cdbase.c のどちらか一方 または両方について,構造体を使わずに, 同等な動作するプログラムを作り直せ.
  6. そして,構造体の有難味を 深く思い知れ.

次回は課題あり. グラフィックスインタプリタ cg.c に 構造体と動的配列を組み込む予定.


(c) 2015, yanagawa@kushiro-ct.ac.jp