配列を使いこなす事で、大規模なプログラムを作っていく事が出来るようになります。特に今回の問18のような使い方は、少なくともゲーム制作においては基本中の基本となる考え方と言ってもよく、こちらの応用でかなりの部分はカバーできると考えています。では、順次説明していきます。


  • 問16. 合計点と平均点
各教科の得点を個別に保存してもしなくてもOKです。その後の拡張を考えてるのであれば、各教科別の得点を配列に保存しておくのが良いとは思いますが、今回は入力後に再度得点を表示する処理は指定されていないため、入力を直接合計得点に加算していく処理としています。

#include <stdio.h> #include <stdlib.h> void main() { // 変数宣言 char strNames[][5] = { "国語", "算数", "理科", "社会" }; int count = _countof(strNames); // 教科数 int nSum = 0; // 合計点 // 得点の入力 for (int i = 0; i < count; ++i) { int score; printf("%sの得点を入力せよ : ", strNames[i]); scanf_s("%d", &score); nSum += score; } // 結果の表示 printf("\n"); printf("四教科の合計は%d点です。\n", nSum); printf("平均は%5.1f点です。\n", (float)nSum / count); }

4教科をループで回して処理するため、各教科の名前に配列を使いました。これすら、直接 printf で書いてしまえば配列は無くても対応できます。このプログラムで配列を使ったメリットとしては、教科名を増やしていくだけで、プログラムは一切変更しなくても対応できるという事でしょうか。例えば strNames に "英語" と追加するだけで、5教科対応となります。

教科数の算出には、以前ちらっと説明した _countof() を使用しています。二次元配列名だけを指定した場合は、最上位の要素数が返却されます。ここを例えば _countof(strNames[0]) と指定すると、第2要素数である 5 が取得されます。_countof() は便利なので、Windows 上でのプログラミングではそれなりに使用頻度は高いです。

あと、注意点は平均点の表示計算で float と明示的なキャストをしておくことでしょうか。このキャストを指定しないと、整数演算と見なされてしまいますので、小数の表示はされなくなってしまいます。


  • 問17-1. ループで文字数カウント
本問題の意図は、同じ結果を求める方法がいくつもあるという事を肌で感じて欲しかったためです。その中でも、セクション 1 のループで文字数をカウントする方法は、一番泥臭い方法です。最後尾の \0 が見つかるまで探しているので、途中もカウントしていますから、現在の検索状態を表示するには良い方法となります。

scanf_s の最大入力文字数にも _countof() を使用しています。カウントは while による無限ループですが、セキュリティを考えて、文字数で数えるのを打ち切るのがベストな実装となります。今回はそれは指定していないため、無限ループとしています。

#include <stdio.h> #include <stdlib.h> void main() { // 変数宣言 char szBuf[128]; // 文字列格納用配列 int nLen; // 文字の長さ // 文字列の入力 printf("何か文字列を入力してください : "); scanf_s("%s", szBuf, (int)_countof(szBuf)); // 文字を数える nLen = 0; while (szBuf[nLen]) nLen++; // 文字数を表示する printf("入力された文字列「%s」は%d文字です。\n", szBuf, nLen); }
※ 健康の指針を示すのはこまめな体温チェックから。気軽に測定できるこちらがオススメです。本当のオススメは口中予測型なのですが、今はまともなメーカーからは売ってないみたいなんですよね。残念です。

  • 問17-2. ライブラリで文字数カウント
セクション 2 のライブラリの使用は最も普通に使用する方法です。文字数を数えるというそのものズバリの関数がライブラリとして C言語には用意されているので、特に理由が無ければこちらの方法を使うのが良いと思います。

なお、\0 が見つかるまで探し続けるという意味では strlen も同様である事から、安全性を加味して探す限界を指定できる strnlen という関数も用意されていたりします。さらに文字列が存在しない場合でもエラーにならない strnlen_s という関数もあります。こんな関数ないかなーとか、こんな関数あったよなーと、自分でライブラリを探す癖が付けば良いなという思いで、このセクション 2 は出題させて頂いています。

#include <stdio.h> #include <stdlib.h> #include <string.h> void main() { // 変数宣言 char szBuf[128]; // 文字列格納用配列 // 文字列の入力 printf("何か文字列を入力してください : "); scanf_s("%s", szBuf, (int)_countof(szBuf)); // 文字数を表示する printf("入力された文字列「%s」は%d文字です。\n", szBuf, strlen(szBuf)); }
strlen を使うとプログラムがもの凄く簡潔ですよね。#include <string.h> が記述されていないとコンパイルは通りません。そして…、このプログラムは VS2022 さまには警告が出てしまうあまりよろしくない処理となっています。
VS2022からの指摘内容
私が息子に問題を提示した20年前は問題ないコードだったんですが、時代が進み問題のあるコードとして指摘されるようになってしまいました。これは \0 が無かったらどうすんの?という警告です。ぶっちゃけそんなん知らんがnうわなにをするくぁwせdrftgyふじこlp

ちょっと横道に逸れますが、VS2022 の指定に従って直してみます。警告が 4、メッセージが 1 です。うち、main の戻り値に云々は今は無視します。すると対応するのはトータルで 4つの指摘事項となります。

書式文字列には '%zd' をお勧めとあります。これは何かと調べてみると、%zd は size_t の型を表示する指定とありました。strlen(szBuf) の戻り値の型が size_t となっていますので、このお勧めが出たのですね。ここは素直に %zd に %d を置き換えます。リビルドするとメッセージが消えて…2つ警告も消えました。

残る警告は zsBuf が 0 で終了しない可能性です。これを考慮したプログラムにせよという事なので、少し考えてみると、配列をゼロクリアしていないのでもし最大サイズの文字列が格納されたらどうすんのって指摘されていると思いました。なので配列の初期化を入れてみました。

#include <stdio.h> #include <stdlib.h> #include <string.h> void main() { // 変数宣言 char szBuf[128] = { 0 }; // 文字列格納用配列 // 文字列の入力 printf("何か文字列を入力してください : "); scanf_s("%s", szBuf, (int)_countof(szBuf)); // 文字数を表示する printf("入力された文字列「%s」は%zd文字です。\n", szBuf, strlen(szBuf)); }
これで全ての警告とメッセージは消えました。特に警告は意味があって表示されていますので、こちらは完全に消し去る癖を付けておきましょう。メッセージはどちらかと言えば今後の対応のために変更した方が良いという指摘です。割と新しい機能に応じた指摘をしてきます。訳あって古い記述にしている場合は、その指摘に従うとハマる事もあります。例えば今回の '%zd' ですが、違う C言語ではサポートされていない事も考えられます。そのため、こちらは考えてから対応するようにしてください。


  • 問17-3. 画面表示で文字数カウント
さて、あまり使う機会がなさそうな printf の戻り値を使った文字数カウントが、セクション 3 です。実は printf は大変多彩で高機能で、画面表示以外にも使用できたりします。標準出力が通常は画面になっているから画面に表示しているだけで、これの出力先がプリンタなら紙に印字されますし、出力先がファイルならファイルに出力されます。
※ 実際にはもう少し複雑ですが概念として説明を聞いておいてください。

そこで出力された文字数が後処理で欲しくなる事があります。そういう意図で、printf の返り値は表示された文字数となっているのです。プログラムは下記の通り。szBuf だけを表示して、その返り値を変数に受け取っています。

#include <stdio.h> #include <stdlib.h> void main() { // 変数宣言 char szBuf[128] = { 0 }; // 文字列格納用配列 int nLen; // 文字の長さ // 文字列の入力 printf("何か文字列を入力してください : "); scanf_s("%s", szBuf, (int)_countof(szBuf)); // 文字数を表示する printf("入力された文字列「"); nLen = printf(szBuf); printf("」は% d文字です。\n", nLen); }
nLen を使わずにこんな書き方も出来ます。

#include <stdio.h> #include <stdlib.h> void main() { // 変数宣言 char szBuf[128] = { 0 }; // 文字列格納用配列 // 文字列の入力 printf("何か文字列を入力してください : "); scanf_s("%s", szBuf, (int)_countof(szBuf)); // 文字数を表示する printf("入力された文字列「"); printf("」は% d文字です。\n", printf(szBuf)); }
この書き方は出来るけどやっちゃいけない例です。パッと見て理解できる構造とは言いがたいからです。このような記述を行うと、バグを作る元になります。今回、あえてダメな例も記載しましたが、これを「どうだ!かっこいいだろ」とか思わないでください。大変迷惑なプログラムになりますので…。
※ 血圧の把握もとても大事です。あまり高いようであれば運動は控えて長引くようなら病院に相談を

  • 問18-1. 敵船の配置
いきなりの実践的な問題です。空母から駆逐艦までサイズはバラバラです。巡洋艦は2隻あったりします。また個別に色も指定されています。関数や構造体などの便利な C言語の機能はまだ習っていないので使わない前提となると、やはり配列をどこまで上手く使えるかがキモになります。

■ 変数宣言

#define FIELD_WIDTH 11 #define FIELD_HEIGHT 9 enum EShip { NONE, CARRIER, BATTLE, CRUISER1, CRUISER2, DESTROY, MAX }; char colors[][10] = { FWHITE, FYELLOW, FCYAN, FGREEN, FGREEN, FBLUE }; int szShip[] = { 0, 5, 4, 3, 3, 2 }; // 船舶サイズ int ofst[][2] = { { 0, 1 },{ 1, 0 } }; // 縦横オフセット EShip nSea[FIELD_WIDTH][FIELD_HEIGHT] = { NONE }; // ゲームフィールド
列挙体で EShip を定義しました。実はこれ、定義だけで使っていなかったりします。ではなぜ定義したかというと、配列の添え字番号にどういう意味があるかを明確化するためです。つまりはプログラマの理解を助けるための記述となります。ステップトレースすると、変数の内容に BATTLE (2) 等と表示されるので分かりやすいです。
トレースでのローカル表示
colors 配列は二次元配列で色指定のエスケープシーケンス文字を用意しました。船舶番号 EShip の値を与えれば、その船舶に必要な色が設定できるようになります。szShip は各船舶のサイズ(長さ)を格納しています。添え字 0 は船舶無しなので、データとしては不要なのですが、-1 のオフセット計算を忘れやすいので、無駄を承知であえて長さ 0 の項目を先頭に入れています。

今回ちょっと特殊なのが ofst という二次元配列です。縦を 0 、横を 1 というデータとして、ofst 配列の第1要素にしていすると、X,Y の方向が取り出せるという配列です。これで、縦でも横でも同じ処理で船舶の存在チェックが出来るようになります。※ 後述

nSea は海戦の舞台となる二次元配列です。海のサイズを後から変更しやすいように、配列サイズは #define で別名定義としてあります。これをどの数値に変えても問題なく動作する…のがベストなのですが、ある程度はそのまま動くはずという作りにしておくのが良いと思っています。

■ 船舶の配置候補座標

まず最初にどこに配置するかを乱数で決めます。

int posX, posY; // 配置座標 int dir = rand() % 2; // 縦配置か横配置か int exist = 0; // 配置済み判定用チェックサム // 配置位置を決める if (dir == 0) { // 縦配置 posX = rand() % FIELD_WIDTH; posY = rand() % (FIELD_HEIGHT - szShip[i] + 1); } else { // 横配置 posX = rand() % (FIELD_WIDTH - szShip[i] + 1); posY = rand() % FIELD_HEIGHT; }
船舶の方向は dir となります。0 と 1 を乱数で決めるので、取得した乱数値を % 2 することで得ています。変数宣言はスコープの最初にまとめる必要があるので、重なり判定用のチェックサム格納用変数 exist もこのタイミングで宣言しています。

配置可能箇所は縦方向か横方向か、あるいは船舶の種別によって長さが変わりますから異なります。例えば横方向の空母だと、X = 0~6, Y = 0~8 の範囲ですが、縦方向の駆逐艦ですと X = 0~10, Y = 0~7 という範囲になります。

具体的には、長さがある方向には、その方向の配列サイズから船の長さを引いて +1 した値で剰余算する事で、配置候補となる座標が得られます。そのため、まずは方向で処理を分岐して、縦配置と横配置で乱数幅を変えています。

■ 船舶が配置可能か

続いて配置が可能かどうかを判定します。

for (int ii = 0; ii < szShip[i]; ++ii) { exist += nSea[posX + ofst[dir][0] * ii][posY + ofst[dir][1] * ii]; } if (exist) continue; // 0以外なら他の船舶と重なっている
配置候補を決める際に縦横で処理を分岐していますので、そのまま方向別に判定も分岐しても良かったのですが、候補座標策定、配置確認、実際に配置するという流れにしたかったので、少し無理をした形で実装しています。ここで生きてくるのが先に定義済みの ofst 配列です。この配列の [0] が X座標のオフセット、[1] が Y座標のオフセットなので、それを基準位置に長さを加味しながら加算していく事で、現在配置検討中の船舶の範囲に、他の船舶が存在しているかどうかを判定できます。

計算式は、基準となる座標+オフセット方向×長さとなっています。その内容を単純に配置判定用の exist 変数に加算しています。他の船舶がいれば exist は 0 以外になります。なので if (exist) で判定して、他の船舶がいれば true と同じ扱いになるので continue; で配置位置に戻すようにしています。

■ 船舶を配置する

ここまで出来たら後は簡単で、配置可能かどうかという処理と全く同様のループで、自分の船舶番号を配置していきます。

for (int ii = 0; ii < szShip[i]; ++ii) { nSea[posX + ofst[dir][0] * ii][posY + ofst[dir][1] * ii] = ship; } break;

ship はループの直後に EShip ship = (EShip)i; として、ループカウンタ変数を事前にキャストしています。さきほどは値の取り出しでしたが、今度は代入という流れになります。最後の break; は配置の無限ループからの脱出となります。
※ 体重も大事ですがそれ以上に大事なのは体脂肪率と内臓脂肪量。それが計測できるのがこちら。本当は全身計をオススメしたいところですが、ちょっと高いんですよね。なのでせめてこれぐらいのレベルでは毎日同じ時間に計測して頂きたいです。
■ 船舶を表示する

全ての配置が完了したら、最後にまとめて表示します。

printf(FWHITE); for (int y = 0; y < FIELD_HEIGHT; ++y) { for (int x = 0; x < FIELD_WIDTH; ++x) { EShip ship = nSea[x][y]; printf(colors[ship]); if (ship == NONE) { printf(" ・"); } else { printf(" @"); } } printf("\n"); }
配置配列 nSea は型が EShip なので、二次元配列から取り出すと既に ship はステップトレースの表示も名前が付きます。ship の種類によって色を変更して、ship が NONE なら ・ を、それ以外は @ を表示しています。表示の際に半角スペースも付与する事で、少し表示的に見やすくしています。一行の表示が終わったら printf("\n"); にて改行しています。


  • 問18-2.  敵船の表示をよりゲームっぽく
これをセクション分けしたのは、汎用的に作られているかどうかを見るためでした。最初の ABCDE.. の表示ですが、簡単に作ろうと思えば…

printf(" | A B C D E F G H I J K\n"); printf("--+----------------------\n");

このように記述すれば出来てしまいます。ですが、ゲームの舞台サイズである FIELD_WIDTH を 11 から 12 に変更したら、この場所は書き換えないと対応できないですよね。そういう事まで意識して組んだかどうかを見るための問題です。という事で、私のコードは以下の通りとなります。

printf("%s |", FWHITE); for (int x = 0; x < FIELD_WIDTH; ++x) { printf(" %c", 'A' + x); } printf("\n"); printf("--+"); for (int x = 0; x < FIELD_WIDTH; ++x) { printf("--"); } printf("\n"); for (int y = 0; y < FIELD_HEIGHT; ++y) { printf("%s%2d|", FWHITE, y); for (int x = 0; x < FIELD_WIDTH; ++x) { EShip ship = nSea[x][y]; printf(colors[ship]); if (ship == NONE) { printf(" ・"); } else { printf(" @"); } } printf("\n"); }

ABC... の表示は、ASCII コードの並びで 'A' に +0,+1,+2,+3 と変化させる事で A,B,C,D と表示を変える事が出来ます。あるいはアルファベットが格納された配列参照でも良いと思います。私のコードだと A~Z までが有効範囲ですが、配列参照にすれば、Z の次は AA 等と自由に拡張できます。どの程度まで拡張を見ておくかは、そのときのゲームデザインなどによって変わると思っています。また、それを判断できるのはプログラマでもあります。

0,1,2,3... の表示は %2dとする事で2桁表示までは表示が崩れないようにしています。こちらは %d だと縦方向を 11 にした途端に画面が崩れますので、あまりよろしくは無いと思います。ゲーム画面で ' 0' と数字が右詰になっているのがミソでした。

ということで、このプログラムであれば、例えば FIELD_WIDTH と FIELD_HEIGHT を 20 に拡張してもそのままこのように動きます。
ゲームフィールドを20x20に変えてみた
如何でしたでしょうか。問18ぐらいになると正解はありません。どのように作れば良いのかと考える事が大事で、それが仕様通りに動いていれば OK です。あとは、経験を積んで、こんな時はこうすると便利だとか、こうすると確実だとか、覚えていく事が大事だと思います。

今回の解答解説はかなり長くなってしまいましたので、サンプルをダウンロードできるようにしておきました。
2022/12/06
※ 空気が汚いと病気になりやすいです。カビの胞子とかハウスダストに乗って何かしらの細菌を吸ったりとか、あまり好ましい状態ではないですね。このダイキンの空気清浄機は長年使っています。ちょっとの匂いでもすぐに反応するなかなかの優れものです。超オススメ!