今回の実力テストは如何でしたでしょうか。意外とあっさり出来ちゃったという人もいれば、しばらくバグに悩んだという人もいたのではないでしようか。大事なのは自分で考えたという経験です。また、私の解説が全てではない点も先に述べておきます。正解はないのです。創意工夫は人の数だけあると言っても過言ではありません。私の解答がなにかの参考になれば幸いです。
- 問19-1. 座標構造体定義
構造体定義の基本なので、これはほとんどの人は問題なかったのではないでしょうか。
// 座標構造体 struct TPoint { int X; // X座標 int Y; // Y座標 }; typedef struct TPoint Point;
メンバ変数の横にコメントを付けておくと、あとで困らないと思います。構造体定義の命名ですが、私は接頭文字として T を付与していたりします。列挙体だと E ですね。ちょっとしたことですが、この接頭文字を付与すると間違えにくいかなと思っています。これを専門用語でプレフィックスと呼びます。
- 問19-2. 艦船構造体定義
この問題は構造体の中に構造体を入れる確認となります。typedef で定義した名前は型と同じ扱いですから、そのままメンバとして記述するだけなので、こちらもさほど問題はなかったのではないでしょうか。
struct TShip { char Name[10]; // 名前 EShip Type; // タイプ Point Position; // 座標 bool IsVertical; // 縦方向か? }; typedef struct TShip Ship;
こちらも問19-1と同様、メンバ変数毎にコメントとプレフィックスを追加しています。
- 問20. 構造体配列定義と初期化
構造体の初期化子の指定の確認です。これが身についていないと、長々と初期化用のコードを書くことになりますので、少し可読性が落ちたりするのではないでしょうか。ただ、個別にメンバを初期化したいこともありますので、この辺りは作るプログラムによって、記述方法を変えるのが良いと思います。
// 艦船 Ship gShips[MAX] { { "空母", CARRIER }, { "戦艦", BATTLE }, { "巡洋艦1", CRUISER1 }, { "巡洋艦2", CRUISER2 }, { "駆逐艦", DSTSHIP }, };
初期化子の適用は、メンバ変数の記述順となります。そのため、構造体定義時に Name と Type を最初に記述しておくのが大切なことになります。例えば Type がもし一番最後に記述されていたとしたら、初期化子で値の初期化をせず、個別に初期化をしたほうが良くなります。
構造体配列変数の記述場所ですが、後述の関数呼び出しでは引数を用意していませんので、どの関数からも使用できるように、グローバル変数エリアに記述することになります。そのため、変数名にプレフィックスとして g という文字を追加しています。これでこの変数はグローバル変数と分かるので、使用時に注意ができるようになると思います。
あと、折角最大値の MAX を用意したので今回は使用していますが、初期化子があれば要素数は省略して記述したほうが便利です。自分が実際に記述するなら [MAX] とせず、[] と書いてしまうと思います。
【Amazon】
AMD Ryzen 5 5600X with Wraith Stealth cooler 3.7GHz 6コア / 12スレッド 35MB 65W【国内正規代理店品】 100-100000065BOX
AMD
2020-11-06
- 問21. 関数定義
まずは GetShipLength をどのように記述したか見てみます。
/// <summary> /// 艦船の長さを取得する /// </summary> /// <param name="type">艦船タイプ</param> /// <returns>艦船の長さ</returns> int GetShipLength(EShip type) { int szShip[] = { 0, 5, 4, 3, 3, 2 }; return szShip[(int)type]; }
問題にしておいた私が言うのもなんですが、この程度の内容であればわざわざ関数にしなくても良いとは思います😓 ここで見てほしいのはコメントです。これ、実は VS2022 はテンプレートが自動的に作られるのです。int GetShipLength(EShip type) という関数エントリを記述した後に、そのすぐ上の行で /// とキーボードから入力すると、自動的にコメント入力用の雛形が作られます。あとはその雛形に説明を書き足していくだけとなります。この雛形を使う最大のメリットは、VS2022 のインテリセンスがそのコメント記述内容を適宜表示してくれるようになるので、使う際にとても便利になることです。最初は慣れないと思いますが、今後は是非使っていくようにしてください。
※ Microsoft Visual Studio 以外ではおそらく通用しませんが…
さて、続いて置き換えです。以前の szShip の場所に GetShipLength を置き換えるとそのまま動きます。動きますが、関数呼び出しのコストが増えてしまうので、実はあまりよろしくないのです。関数呼び出しの引数は艦船タイプである EShip です。この値はループカウンタ変数 i で固定ですから、ループの直後にローカル変数に代入してしまえば、後はいちいち関数を呼び出さなくてもよくなります。結果、実行速度の向上が見込めるのです。ということで、置き換えた私の解答例を載せます。
int ofst[][2] = { { 0, 1 },{ 1, 0 } }; // 縦横オフセット srand((unsigned)time(NULL)); for (int i = 0; i < (int)MAX; ++i) { EShip ship = (EShip)(i + 1); // 配置対象船舶番号 int len = GetShipLength(ship); // 艦船の長さ while (true) { int posX, posY; // 配置座標 int dir = rand() % 2; // 縦配置か横配置か int exist = 0; // 配置済み判定用チェックサム // 配置位置を決める if (dir == 0) { // 縦配置 posX = rand() % FIELD_WIDTH; posY = rand() % FIELD_HEIGHT - (len - 1); } else { // 横配置 posX = rand() % FIELD_WIDTH - (len - 1); posY = rand() % FIELD_HEIGHT; } // 配置が可能か for (int ii = 0; ii < len; ++ii) { exist += gSea[posX + ofst[dir][0] * ii][posY + ofst[dir][1] * ii]; } if (exist) continue; // 0以外なら他の船舶と重なっている // 配置する for (int ii = 0; ii < len; ++ii) { gSea[posX + ofst[dir][0] * ii][posY + ofst[dir][1] * ii] = ship; } break; } }
艦船長さはループ開始直後に len というローカル変数に入れており、あとは必要に応じてこのローカル変数 len を参照しています。実はこのような書き方とすることで、CPU のキャッシュに収まる可能性が高まり、より高速動作が期待できるようになります。なるべくローカル変数内で処理が出来るようにすると、より高速動作が期待できると言い換えてもいいかな。
まあ、今どきの CPU だとその実行速度差は誤差範囲ですが、小さな小さな誤差の積み重ねが最後に大きな差になることもあります。プログラムの可読性(読みやすさ)や可用性(メンテナンスしやすさ)が犠牲にならない範囲でなら、ローカル変数に置き換えるのは簡単で有効な手段と言えます。
- 問22. 艦船の配置を反映する
これは問21のプログラムの流れをどこまで理解しているのかという確認問題でした。そのため、動作を理解している人にとってはめちゃくちゃ簡単だったのではないかと思います。逆にここで悩んだ人は、プログラムの流れを掴みきっていなかったのではないかと。
とはいえ、問題提示の時に「追加するプログラムを最後の break; の直前とする。」とヒントというより答えを書いちゃったので、これはボーナス問題だったかもですね😓 追加するプログラムは以下の通りです。
// 艦船の配置情報を保存する gShips[i].Position.X = posX; gShips[i].Position.Y = posY; gShips[i].IsVertical = dir == 0; break;
あと、この問題は構造体の中の構造体のメンバの値を書き換えるテストでもありました。ドットで繋げるんですよね。分かってしまえばとてもわかりやすいのではないかと思います。IsVertical は bool 型ですので、こちらの値を入れるのだけが、慣れないと少し悩むポイントです。dir が 0 なら true を入れるのですが、判定式の結果は true / false になっていますので、そのまま右辺に判定式を書いてしまえば、判定結果の true または false が入っていきます。
- 問23. プログラムの全体構造
ゲームフィールド gSea と、艦船情報 gShips はグローバル変数で宣言されていますから、この問題はどこからどこまでを関数として分断するかという問題でした。意外と書き換える場所が少なくて、コピー・ペーストがほとんどだったのではないかと思います。今回はテスト問題として、また、関数の解説がないプログラムからの分断ですから、このように後から関数分離としていますが、今後は是非機能別に関数分解しながら作るようになると嬉しいです。
※ この機能別に関数分解していく作り方を、構造化プログラミングと呼んでいます。
この問23の解答はプログラム全域になり多いので、詳細は以下のプログラムをダウンロードして確認してみてください。
2023/02/10
コメント
コメント一覧 (2)
内藤時浩
が
しました