異種・複数のデータを ひとつにまとめたものが構造体である. 構造体を有効に活用すれば, 多くの変数を必要とするような 複雑な処理を 少しの変数だけで簡潔に記述できるようになり, プログラム開発作業の大幅な効率化につながる.
具体的に,たとえば, ある種のゲームプログラムの開発していると想定してみよう.
このゲームに登場するキャラクタは, 様々な属性{名前,位置,姿勢,体力,所持品,...}を持つとする. それらの属性を別個の変数で表わすとしたら, 関数呼び出しの度に,一体いくつの変数を渡さなきゃならないんだ? 開発の進行にともなって, 呼び出したい関数もたくさん増えて来たし, キャラクタも多数登場させたいんだが... これは面倒くさいことになりそうだ...
属性毎に個別の変数を多数使うのではなく, キャラクタを表わす構造体の変数を1個だけ使って済ませよう. これで面倒や間違いから開放され,幸せになれる.
もちろん,タダで楽をすることはできない. たくさんの勉強が必要となるが, 一度理解してしまえば後々快適. 永く楽をするための苦労を惜しまないこと.
教科書(K&R) pp.154-165 も参考にしよう.
構造体の説明の前に, 新しいデータ型を作る方法 typedef を紹介しておく.
typedef 既存の型名 新しい型名; // 新しい型の定義 新しい型名 変数名, ...; // 新しい型の変数の宣言
typedef int Price; // Price型の定義 Price tomato, potato; // Price型の変数 tomato, potato の宣言
この具体例では int 型を利用して Price(価格)型を新たに定義している. このようにしておけば,変数 tomato,potato が 何を表わす変数だったのか?少しだけ,わかり易くなっている.
一方,int tomato と書いた場合, トマトの何だった?重さ?大きさ?と混乱してしまうかもしれない. 「3 日後の自分は他人.」
なお,この考え方の度が過ぎて... 「変数名や関数名には型名も含めなきゃ不十分だ」 と主張する人達もいます. たとえば:
Price price_tomato, price_potato;
ある時期の Windows の開発者達は, このような流儀(a.k.a. ハンガリアン方式) を採用していた.
一方,Linux の作者達は,「そんなのは無駄」と批判していた. この流儀は「頭の頭痛が痛い」みたいで 冗長だし. 「過ぎたるは及ばざるがごとし.」
どちらにせよ, わかりやすく書きやすい名前(=誤解を生まないような変数名や型名) をつけるのが良い. 具体的にどうすれば? ...プログラマのセンスが問われる問題だ.
では,typedef の実際の使い方を理解するために, List 1 を試してみよう.
#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 型の変数とまったく同様に, 整数値の代入や,整数としての入出力が可能である.
構造体を使うには, まず,複数のデータ型を組み合わせて構造体のデータ型を定義し, それから,その型を使って構造体の変数を宣言することになる. これで,複数のデータをひとつの変数にまとめられるようになる.
以下では具体例として,野菜情報(価格,重量,生産者名,等の組み合わせ)と 複素数(実数データと虚数データの組み合わせ)を採り上げ, 構造体の定義方法を説明する.
構造体の基本的な定義方法がこれだ.
struct タグ { // 構造体型の定義 型1 メンバ1; 型2 メンバ2; ... }; struct タグ 構造体変数; // 構造体変数の宣言
ここで,メンバ(member)とは要素データの名前, タグ(tag)とは構造体の名前(集合の名前)である. 構造体変数の型は「struct タグ 型」である.
struct vege { // 野菜型の定義 int price; // 価格 double weight; // 重量 char *farmer; // 生産者名 }; struct vege tomato, potato; // 野菜型変数 tomato, potato の宣言
struct complex { // 複素数型の定義 double re; // 実数部(real part) double im; // 虚数部(imaginary part) }; struct complex z; // 複素数型変数 z の宣言
typedef を利用した構造体変数の定義もよく使われる.
typedef struct { // 構造体型の定義 型1 メンバ1; 型2 メンバ2; ... } 構造体型; 構造体型 構造体変数; // 構造体変数の宣言
この方法では,struct の後のタグを省略できる. (記述してもよい.)
typedef struct { // 複素数型の定義 double re; double im; } Complex; Complex z; // 複素数型変数 z の宣言
この例では, struct と typedef の合わせ技で, Complex 型 を定義し, さらに Complex 型 の変数を定義している. (長たらしい「 struct Complex」型ではなく, 単に「Complex」型.)
タグ方式でも typedef 方式でも,どちらを使っても構わない. コーディング作業でのこれら 2 つの方式の違いは, 構造体の定義時にタグか typedef のどちらを付けるのかと, 構造体変数の宣言時に struct を付けるかどうかだけ.
この授業では,主として,typedef 方式を使う. 変数宣言の際,いちいち struct を付けるのが面倒なので...
補足(上級者向け): タグの省略が不可能な場合もある. たとえば,構造体を再帰的に定義する (その構造体のメンバ変数として同じ構造体型を含める) ような場合.
テストプログラムは,後々のセクションで... しばらく,ややこしい理論説明が続くが, 効率良くプログラミングする (i.e. すごいプログラムを楽に作る) ために必要な知識となるハズなので, 読み飛ばさないこと.
構造体変数へデータを代入する方法を説明する.
構造体変数も通常の変数や配列と同様に, 宣言と同時に初期化できる.
構造体型 構造体変数 = { 値1, 値2, ... };
ちなみに,構造体変数の各メンバの変数は, 構造体変数.メンバのようにして指定できる. したがって,上の初期化処理は,次と同じことになる:
構造体型 構造体変数; 構造体変数.メンバ1 = 値1; 構造体変数.メンバ2 = 値2; ...
Complex z = { 1.0, 2.0 };
これは,次と同じことである:
Complex z;
z.re = 1.0;
z.im = 2.0;
// z = {1.0, 2.0}; // これはNG
残念ながら,構造体変数の全メンバへの一括代入は, 宣言文以外ではできない.
構造体型 構造体変数1 = { 値1, 値2, ... }; // OKだが実は例外的な措置(配列と同様)
構造体型 構造体変数2;
構造体変数2 = { 値1, 値2, ... }; // これが NG なのは不便...
構造体変数2 = 構造体変数1; // ...だがこれは OK
だが,構造体同士の代入は可能なので, 構造体の初期化処理では,次のように, 初期化関数を利用すると便利である:
構造体型 初期化関数(型1 仮引数1, 型2 仮引数2, ...) { 構造体型 構造体変数; 構造体変数.メンバ1 = 仮引数1; 構造体変数.メンバ2 = 仮引数2; ... return (構造体変数); // こんな初期化関数を作っておけば... } 何らかの関数() { 構造体型 構造体変数; // 構造体変数 = { 値1, 値2, ... }; // これは NG だったが... 構造体変数 = 初期化関数(値1, 値2, ...); // ほぼ同様な記述が OK に ... }
Complex ComplexInit(double re, double im) { Complex z; z.re = re; z.im = im; return (z); } int main() { Complex z1; // z1 = {1.0, 2.0}; // NG... z1 = ComplexInit(1.0, 2.0); // z1 = 1 + 2i printf("z1 = %f + %f i\n", z1.re, z1.im); return (0): }
初期化関数を定義するのは,面倒くさそうなので,最初は嫌かも. 小さなプログラムならば, 初期化関数を使わず,メンバ毎の代入の方が楽だし...
しかし,プログラムの開発が進むにつれ, 構造体変数やメンバの個数が多くなってくると, ソースコードの長さが爆発的に増大してしまうことになる. それは,もっと嫌だよね?
「急がば回れ.」 初期化関数は必要悪. というか,重要な初期投資だ.
配列は同じ型のデータ同士の集合 (例:int 型だけ10個とか)である. 一方, 構造体は異なる型のデータの集合 (例:int 型と double 型の組み合わせ等)である. 混同しないこと.
複素数の例の場合,同じ型のデータの集合 (メンバ re も im も実数型)なので, 構造体ではなく,配列によって表現することも可能だ. しかし,配列ではデータをまとめてコピーするようなことはできない:
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() の変換指定子は 組み込み型(char,int,double,等) だけにしか対応していないためである:
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++; }
次の動物データベースプログラムの例を試してみよう.
次回は課題あり. グラフィックスインタプリタ cg.c に 構造体と動的配列を組み込む予定.