前回、オブジェクト単位にファイルの分けようというお話をしました。それぞれのオブジェクトには、色んな要素があります。それを一つ一つ変数で用意すると、管理がとても煩わしくなります。それは、C言語 実力確認テスト05 配列で、敵船の配置プログラムの作成に取り組んで頂いた方ならご理解頂けるかと思います。今回は、そのオブジェクト単位で各要素をまとめて定義して管理出来る構造体の解説となります。
--
いきなりここに飛んで来ちゃった人は、よろしければ下記からご覧ください。

  • 構造体とは
敵船の配置では、登場船舶オブジェクトは以下の要素を持っていました。
  1. 名前
  2. タイプ
  3. サイズ
  4. 配置座標
  5. 配置の向き
これら、オブジェクトの各要素の事をプロパティと呼びます。このプロパティは、別々の配列で用意して管理していたため、各配列に対して個別に参照して情報を取り出していました。例えば、2隻目の船舶データを取り出すには、[2] という添え字の指定を各配列に対していちいち行っていました。

この複数の配列は全部「船舶」というオブジェクトの要素(プロパティ)です。そのため、最大数は同じです。これらのプロパティをまとめて管理出来た方が便利だし分かりやすいです。そこで使用するのが構造体となります。構造体は以下の構文を持ちます。

struct 構造体名 { 型 メンバ変数名; 型 メンバ変数名; 型 メンバ変数名; : : };

構造体で定義されたプロパティはメンバ変数と呼ばれています。メンバはいくつでも記述する事が出来ます。例えば、上記に示した船舶は以下のような記述となります。

enum EType { CARRIER, BATTLE, CRUISER, DESTROY }; struct TShip { char Name[10]; // 名前 EType Type; // タイプ int PosX; // X座標 int PosY; // Y座標 bool IsVertical; // 縦方向か? };
これで、TShip いう構造体定義が出来ました。これは、Name 等のメンバ変数を持っている TShip という新しい型を作った事と同意となります。そのため、船を6隻用意したければ、

const int TotalShip = 6; struct TShip Enemy[TotalShip];
このような記述をすれば共通のプロパティを持った6隻の船舶オブジェクトが作れてしまいます。以前と比べると 1行で完結しますから、とても見やすいですよね。なお、いちいち struct EShip と記述するのが面倒な場合は、typedef という別名定義を使う事で、より簡単に記述出来るようになります。

typedef struct TShip Ship;
これは、struct TShip を Ship という名前で別名定義するという宣言です。この記述をしておく事で、先ほどの少し煩わしかった宣言も…

const int TotalShip = 6; Ship Enemy[TotalShip];
この通りすっきりと記述する事が出来ます。


  • 構造体の初期化
構造体は宣言しただけでは中身は未定義です。そのため、初期化が必要となります。変数では int num = 5; 等と記述すれば、変数宣言時に同時に値を設定する事が出来ました。構造体でも同様の事が出来ます。但し、複数のメンバがあるので、{} で区切って初期値を与える事になります。とりあえず、船舶の中から「空母」の初期化をしてみます。

Ship spCarrier { "空母", CARRIER, 0, 0, false, };
記述したメンバ数の順番に、その型に応じた初期値を ,(カンマ)で区切って並べるだけで完了します。いちいちメンバ変数に対して初期化をする必要がない分、記述も簡単になります。初期化子が途中までしか指定されなかった場合は、それ以降のメンバは全て 0 が設定されます。全く何も初期化子がない場合は不定値です。そのため、座標と方向が 0 で良いのなら…

Ship ship { "空母", CARRIER };
この記述で初期化が完了します。配列の場合は、その配列要素の塊に応じて {} の括りを増やす事になります。全ての艦を作ってみます。

Ship ships[] { { "空母", CARRIER }, { "戦艦", BATTLE }, { "巡洋艦1", CRUISER }, { "巡洋艦2", CRUISER }, { "駆逐艦", DESTROY }, };
なお、最初にいきなり座標と向きを乱数で入れる事も出来るのですが、縦配置と横配置で初期化する乱数の幅が異なりますから、単純に初期化子に乱数生成を入れる事は出来ません。この場合は構造体配列の宣言後に配置処理を記述すると良いと思います。
※ 美味しい…ですが、それよりもお目当ては缶ケース。電波遮断間違いないので、自動車のスマートキーを入れておくとリレーアタックを防止出来て宜しいかと。ケースのデザインもかっこいいし。オマケに美味しいしw

  • 構造体変数の使い方
メンバにはピリオドを書いて読み書きが出来ます。例えば、2隻目の船名を表示するのであれば、

printf("艦名: %s", ships[2].Name);
このように構造体変数(この場合は配列ですが)の後方にピリオドを追加して、続けてメンバ名を記述すれば取り出せます。このピリオドの事はドット演算子と呼ばれていますが、うん、覚えなくてもきっと困らないです😁

代入も同様にドット演算子を付けて左辺に書きます。

ships[0].PosX = rand() % FIELD_WIDTH; ships[0].PosY = rand() % FIELD_HEIGHT;
これで座標にランダムな値が格納されます。尤も、このままでは艦の方向と長さが考慮されていないため、ちょっとマズい事になりますが、まあサンプルと言う事で…。

--

構造体の中に構造体を用いる、構造体の入れ子にする事が出来ます。かなり積極的に使う事で、より汎用的な記述が出来るようになります。例えば、さきほどの TShip 構造体ですが、そのメンバに PosX, PosY という変数がありました。これ、本来は座標として一括りにするべきなので、新たに TPosition という構造体を定義してしまいます。

struc TPoint { int X; int Y; }; typedef struct TPoint Point;
これで座標を示す構造体 TPoint が出来ました。これを先の TShip 構造体に入れてみましょう。

struct TShips { char Name[10]; // 名前 EType Type; // タイプ Point Locate; // 配置座標 bool IsVertical; // 縦方向か? };
座標の値を取り出したい場合は、ドット演算子を並べて書きます。

printf("X: %d, Y:%d", ships[0].Locate.X, ships[0].Locate.Y);
この辺りの記述方法はインターネットの url に似ていますね。

--

構造体は変数なので、普通にまとめて代入が出来ます。例えば、ships の初期化後であれば

Ship dup = ships[1];
このように変数 dup に配列 ships[1] の内容を代入する事が出来ます。複写されていますので、sup.Locate.X = 50; 等と値を入れると、dup の配置座標 X は 50 に変更されますが、元となっている ships[1].LOcate.X 側は最初の 0 のままとなります。


  • Tips
※ メンバ変数とプロパティ
メンバ変数は構造体の中に定義された変数を指します。プロパティとは、オブジェクトを構成する要素となります。C言語の文法的にはメンバ変数しかありませんが、オブジェクトの各要素という意味で使う場合はプロパティとなります。この呼び方の違いがよく分からないという場合は、C言語においては全てメンバ変数と言うようにしてください。

この部分、ブログ記事の書き方としてはどうしようかと悩みました。C言語講座だから、全部メンバ変数と記述するのが本来は正確です。ですが、概念としてプロパティという言葉を使いたかったので、このような記述としてしまいました。この説明では初心者は納得しづらいとは思いますが、こういう呼び方もあるのか程度に理解してもらえると嬉しいです。

※ 構造体に艦の色や長さを用意しないワケ
艦の色や長さは艦のタイプにより固定です。そのため、EType が分かれば色も長さも確定します。同じ意味を持つメンバを複数保持すると、整合性が取れなくなるバグが出やすいのです。まとめて情報がメンバとして記述出来るからと、何でもかんでもそこに入れれば良いというモノでもないです。最初の頃はこの判断は難しいかもしれませんが、徐々に慣れていくとよろしいでしょう。

なお、計算結果を減らすために敢えてメンバに重複した情報を記述する事もあります。Etype から長さや色が取得出来るとは言え、別の配列からデータを拾ってくる事になりますから、処理速度的には低下します。この敢えて重複したデータをメンバに持つのは、ゲームプログラムにはよくあったりします。逆にシステム系のプログラムでは御法度です。プログラムの実装対象によって良い悪いの判断が変わるのです。

※ お菓子なんか要らねーからとにかくスマートキーの電波を遮断したいんだよって場合は、こちらなんて如何でしょうか。割と格好いいと思います。