A問題(50点)
構造体を関数に渡す方法とアロー演算子の使い方について確認する。
以下のような構造体を宣言し、指示に従ってプログラムを作成せよ。 (提出ファイル名 prog01a.c, prog01b.c, prog01c.c)
typedef struct{
int id; /* ID */
char name[12]; /* 名前 */
int year; /* 学年 */
char subject[12]; /* 科目名 */
char grade; /* 成績 */
}StudentRecord;
A. 構造体を定義・初期化し、関数を使って表示する。student1とstudent2を初期化せよ。
/* 以下の構造体の初期化はmain()の中に書くこと */
StudentRecord student1 = {学生1のデータ};
StudentRecord student2 = {学生2のデータ};
void PrintInfo(StudentRecord);
void PrintInfoPtr(StudentRecord *);
student1とstudent2の内容を出力するようにせよ。
また、それぞれの関数での出力が同じであることを確認せよ。 % ./a.out 構造体の値渡し (PrintInfo) 1319991 Fukushima 3 Literacy B 1319992 Wakamatsu 2 Programming A 構造体のアドレス値渡し (PrintInfoPtr) 1319991 Fukushima 3 Literacy B 1319992 Wakamatsu 2 Programming A %B. 次に
prog01a.cをprog01b.cにコピーし、
構造体の宣言はそのままに、構造体配列を使ったものに変更する。studentdataを初期化する。
/* 構造体配列をmain()中で以下のように初期化する */
StudentRecord studentdata[2] = {
{学生1のデータ},
{学生2のデータ}
};
PrintInfo(), PrintInfoPtr()を使って表示しなさい。
関数はprog01a.cのものをそのまま使用する。prog01b.cをprog01c.cにコピーした上、
構造体配列の要素数を増やし、入力用関数でデータを読み込み、表示する。InputInfo()を追加する。
この関数は、一人分のデータを読み込む関数として実装すること。さらに、
関数のプロトタイプは次のようなものであるとし、戻り値はscanfの戻り値に準拠すること:int InputInfo(StudentRecord *);
data.txt(/home/course/prog1/public_html/2025/ex/ex08/data.txtにファイルがあるのでコピーすること)からリダイレクションで読み込むものとする。
ただし、最大データ数は20として、マクロNで設定すること。PrintInfo(), PrintInfoPtr()を使って表示しなさい。
関数はprog01a.cのものをそのまま使用する。data.txtは,データ区切りに空白を使用している. scanf使用時で文字を一文字読み込むために入力フォーマット指定子の%cを使用した場合, 区切り文字に使用しているスペースや改行等も一文字として読み込もうとする. このため, 入力フォーマット指定子%cの前に一つスペースを入れて データ区切りの空白を読み飛ばすように工夫をすること. %d, %lfで数値を読み込む場合や, %sで文字列を読み込む場合は, 入力フォーマット指定子の前後に空白を開けても開けなくても数値や文字列 (文字列の終わりは空白や改行で示される)を指定通り読み込んでくれる.
実行例:% ./a.out < data.txt 構造体の値渡し (PrintInfo) 1319991 Fukushima 3 Literacy B 1319992 Wakamatsu 2 Programming A 1319993 Kitakata 3 Math B 1319994 Tajima 3 English C 1319995 Iwaki 3 Japanese B 構造体のアドレス値渡し (PrintInfoPtr) 1319991 Fukushima 3 Literacy B 1319992 Wakamatsu 2 Programming A 1319993 Kitakata 3 Math B 1319994 Tajima 3 English C 1319995 Iwaki 3 Japanese B
B問題(50点)
以下のように、2次元平面上の点を表すためにXY型の構造体と
それを使って円を表すためのCIRCLE型構造体を宣言した。
CIRCLE型構造体は中心点をXY型構造体,半径、直径、周長と面積をそれぞれdouble型の変数で表現している。
typedef struct{
double x; /* x座標 */
double y; /* y座標 */
}XY; /* 平面上の点 */
typedef struct{
XY center;
double radius;
double diameter;
double circumference;
double area;
}CIRCLE; /* centerを中心点、radiusを半径とする円。diameterは直径、circumferenceは周長、areaは面積 */
この構造体を使用して2次元平面上の円の情報をキーボードから入力させ、その内容を表示するプログラムを作成しなさい。
(提出ファイル名 prog02.c)
2次元平面上の円は、中心の座標と、その円周上の任意の1点の座標が決まれば定義できるので、これらを入力させることにする。 半径や直径、周長、面積は入力後に計算して求めるものとする。
円の定義情報の入力は、以下のプロトタイプ宣言例にあるように、 値の受渡し方法が異なる2種類の関数を作成して用いる。
CIRCLE inputCircle(void); /* データを読み込んだ構造体を戻す */ void inputCirclePtr(CIRCLE *); /* 構造体のポインタを渡し、そこにデータを読み込む */
どちらの関数も、円の定義情報の入力の受付と、それに基づく円の情報のCIRCLE型構造体の変数への格納を行う。このとき、円周上の1点の座標は、CIRCLE構造体のメンバとして格納することはできないため、入力処理を行う関数の中で別途XY型構造体変数を定義して使うと良い。
main関数の中ではこれらの関数をそれぞれ1度づつ呼び出し、その結果を表示する。
実行例:% ./a.out 構造体を返す関数 (inputCircle): 円の中心点の座標と円周上の1点の座標を入力: 0.0 0.0 2.0 0.0 Circle1: Center, Radius : (0.000000, 0.000000), 2.000000 Diameter, Circumference, Area : 4.000000 12.566371, 12.566371 構造体ポインタを使う関数 (inputCirclePtr): 円の中心点の座標と円周上の1点の座標を入力: 0.0 0.0 2.0 0.0 Circle2: Center, Radius : (0.000000, 0.000000), 2.000000 Diameter, Circumference, Area : 4.000000 12.566371, 12.566371 % ./a.out 構造体を返す関数 (inputCircle): 円の中心点の座標と円周上の1点の座標を入力: -2.0 -2.0 -2.0 -3.0 Circle1: Center, Radius : (-2.000000, -2.000000), 1.000000 Diameter, Circumference, Area : 2.000000 6.283185, 3.141593 構造体ポインタを使う関数 (inputCirclePtr): 円の中心点の座標と円周上の1点の座標を入力: -2.0 -2.0 -2.0 -3.0 Circle2: Center, Radius : (-2.000000, -2.000000), 1.000000 Diameter, Circumference, Area : 2.000000 6.283185, 3.141593
本プログラムにおいて、math.hをインクルードすることで,
円周率を表すマクロM_PIや、平方根を計算するsqrt関数を使用できる。
math.hをインクルードする場合,コンパイル時にはgcc -lm prog02.cのように
-lmオプション指定を加えるのを忘れないこと。
N(マクロで定義する)回発生させ、
各出目の出現頻度を調べるプログラムを作成する。
これらのデータは以下の構造体に格納するものとする。指示に従って順を追ってプログラムを完成させよ。
#define N 600000 /* 試行回数 */
typedef struct{
int data[N]; /* N個の要素を持つ配列 */
int count[6]; /* 各出目の回数 */
}DiceRollData;
srand関数を呼び出して乱数の初期値として
time関数を用いて得た現在時刻を与える
(乱数の初期値を与えることを「乱数の種を作る」とも言う。
これはrand関数で乱数を作る前に一回だけ行えばよい)。srand((unsigned int)time(NULL));
注意:time関数の使用の際にはtime.hを、
またsrand関数とrand関数を用いる際には
stdlib.hをインクルードする必要がある。
rand関数は0からマクロRAND_MAXまでの範囲の
整数値の乱数(正確には疑似乱数である。問題文の下の注記を参照のこと)を返すので、
以下のようにすれば0から5までの整数値の乱数を生成できる。
これをN回繰り返して、DiceRollData型構造体の変数
(変数名は任意)のメンバである配列data[N]に格納する。rand() % 6;
DiceRollData型構造体の変数のメンバである
配列count[6]をすべて0で初期化する。DiceRollData型構造体の変数に格納された乱数の各出目の出現頻度を調べ、
メンバの配列count[6]に格納する関数ComputeStatisticsPtrを作成せよ。
関数のプロトタイプは以下のようなものであるとする:void ComputeStatisticsPtr(DiceRollData*);
ComputeStatisticsPtrを呼び出して、出目の出現頻度を調べ、その結果を
main関数内で表示せよ。実行例(乱数によるプログラムなので、表示される数値は毎回異なる):
% ./a.out [0]: 100468 [1]: 99540 [2]: 100101 [3]: 100352 [4]: 99671 [5]: 99868
実行して得られる出目の出現頻度の偏りは、試行回数Nが少なければ大きくなり、多くすれば小さくなる。ただし、自動変数として宣言可能な配列変数、構造体変数のサイズには上限(環境によって異なる)があるので、Nを極端に大きくするとコンパイルに失敗したり、実行時にエラーが発生することがある。第6回で学んだ動的メモリ割り当てを使うと、この制限を超えることができる。
ComputeStatisticsPtr関数を元に次のような関数を作成し、
こちらを用いても同様に動作することを確認せよ(ComputeStatisticsPtr関数は消さずにプログラム中に残しておくこと)。DiceRollData ComputeStatisticsV(DiceRollData);
実行例は省略
ComputeStatisticsPtrと値渡しの関数ComputeStatisticsVそれぞれの実行時間を比較する。ハンドアウトp124、Lec08-20のlec08-7.cを参考に、それぞれの実行時間を計測して結果を出力できるようにせよ。このとき、一度生成した乱数を両方の関数で処理してもよいし、それぞれの関数用に別の乱数セットを生成してもよい。DiceRollData型構造体の変数そのものも再利用しても構わないが、その場合は出目の回数情報を再初期化することを忘れないこと。実行例(乱数によるプログラムなので、表示される数値は毎回異なる):
% ./a.out ComputeStatisticsPtr (Call by address): [0]: 99767 [1]: 100326 [2]: 100063 [3]: 99916 [4]: 99858 [5]: 100070 ComputeStatisticsV (Call by value): [0]: 99942 [1]: 99509 [2]: 100567 [3]: 100309 [4]: 100010 [5]: 99663 --- time --- Call by address: 0.001270 sec Call by value: 0.003314 sec
コンパイラには、実行ファイルを最適化して処理速度を向上させる最適化オプション-O2などが
存在するが、今回のように処理速度の差を比較する場合は最適化オプションは付けずにプログラムをコンパイルすること。
参考: 疑似乱数は、一見乱数のように見えるが確定的な計算によって求められている。そのため、作成方法などが
わかっていれば、同じ結果を再現することも可能であり、次にどんな値が出現するかも予測可能な場合がある。
ここで用いているC言語標準のrand関数、srand関数を用いた疑似乱数生成法では、
srand関数に与える種の値が同じであれば、同じ値が生成されることになる。これは一見デメリットの
ようにも見えるが、計算機シミュレーションなどにおいては同じ結果を再現できるというメリットにもなる。
rand関数、srand関数を用いる方法は簡便ではあるが、得られる乱数の質はあまり良くな
い(ここでいう品質とは、出現する値にある種の規則性が生じるなどの問題を指す)。
実用的には他の方法で乱数を発生させることがよく行われるので、興味がある人は「メルセンヌ・ツイスタ」などの
キーワードをもとに調べてみるとよい。
Extra問題
問題2では構造体CIRCLEを利用して円を表現したが、
それに高さを加えたCYLINDER型の構造体を作成し、
円柱の表面積と体積を計算し表示するプログラムを作成する。
なお、問題2で作成した二種類の入力関数をもとに、円柱データに対応するように修正した二種類の関数を作成して用いること
(底面の円の半径の値を計算し,メンバ変数にその値を代入する処理も行うこと)。
また、以下に示す構造体CYLINDERを追加する:
typedef struct{
CIRCLE circle;
double h;
double surface;
double volume;
}CYLINDER; /* circleを底面、hを高さとする円柱。surfaceは表面積、volumeは体積 */
実行例:
% ./a.out 構造体を返す関数: 円柱底面の円の中心点の座標と円周上の1点の座標を入力: 0.0 0.0 1.0 0.0 円柱の高さを入力: 1.0 Cylinder1: Radius, Height : 1.000000, 1.000000 Surface, Volume : 12.566371, 3.141593 構造体ポインタを使う関数: 円柱底面の円の中心点の座標と円周上の1点の座標を入力: 0.0 0.0 0.0 2.0 円柱の高さを入力: 1.5 Cylinder2: Radius, Height : 2.000000, 1.500000 Surface, Volume : 43.982297, 18.849556 % ./a.out 構造体を返す関数: 円柱底面の円の中心点の座標と円周上の1点の座標を入力: 1.0 1.0 1.0 3.0 円柱の高さを入力: 1.0 Cylinder1: Radius, Height : 2.000000, 1.000000 Surface, Volume : 37.699112, 12.566371 構造体ポインタを使う関数: 円柱底面の円の中心点の座標と円周上の1点の座標を入力: 1.0 1.0 1.0 3.0 円柱の高さを入力: 1.0 Cylinder2: Radius, Height : 2.000000, 1.000000 Surface, Volume : 37.699112, 12.566371 %
#define N 10 /* 盤面の大きさ */
typedef struct{
int bomb; /* 地雷の有無(0 → 無、1 → 有) */
int bombCount; /* 周辺の地雷の数 */
int open; /* 開いているかどうか(0 → 閉、1 → 開) */
int flag; /* 旗の有無(0 → 無、1 → 有) */
}Status;
typedef struct{
Status status[N][N]; /* 盤面 */
int flagCount; /* 全ての配置した旗の数 */
int correctCount; /* 正しい配置をした旗の数 */
int bombs; /* 設置する地雷の数 */
}Map;
コンソール上で動くマインスイーパーを作成します。
template05.cにテンプレートを用意しています。
マインスイーパーとは、ある大きさの盤面に隠された地雷をすべて見つけるゲームです。
盤面は長方形であり、いくつかのマスに区切られています。
ユーザーはそれぞれのマスに対して、「開く」と「旗を置く」という操作をすることができます。
開く操作を行うと、そのマスに地雷がなければ、
上下左右斜めの8マスの中で開けたマスの周りのマスにいくつ地雷があるかを数で表示します。
開けたマスに地雷があれば、ゲーム終了となります。
旗を置く操作を行うと、そのマスに旗が置かれます。
すべての地雷に旗を置くと終了となりますが、地雷のない場所に旗が置いてあると、
ゲーム終了とはなりません。以上を含んだゲームの仕様を示します。
template05.c内のコメントと下記を参考にプログラムを完成させてください。void Initialize(Map*)関数Mapの初期化を行う。
設置する地雷の数、地雷の座標を乱数を用いて決めること。int Judge(Map*)関数void View(Map)関数void Open(Map*)関数% ./a.out Flag/Bomb:0/8 \ 0123456789 \========== 0|----------| 1|----------| 2|----------| 3|----------| 4|----------| 5|----------| 6|----------| 7|----------| 8|----------| 9|----------| ========== Input : i(0=open/1=flag) x y Input : 0 0 0 Flag/Bomb:0/8 \ 0123456789 \========== 0|0---------| 1|----------| 2|----------| 3|----------| 4|----------| 5|----------| 6|----------| 7|----------| 8|----------| 9|----------| ========== Input : i(0=open/1=flag) x y Input : 0 1 1 Flag/Bomb:0/8 \ 0123456789 \========== 0|0---------| 1|-0--------| 2|----------| 3|----------| 4|----------| 5|----------| 6|----------| 7|----------| 8|----------| 9|----------| ==========~(省略)~Input : i(0=open/1=flag) x y Input : 1 4 8 Flag/Bomb:1/8 \ 0123456789 \========== 0|000-------| 1|000-------| 2|110-------| 3|-10-------| 4|-10-------| 5|-10-------| 6|-100000---| 7|-223210---| 8|----F10---| 9|-----10---| ==========~(省略)~Input : i(0=open/1=flag) x y Input : 1 7 1 --CLEAR-- \ 0123456789 \========== 0|000----11-| 1|000----x10| 2|110--1x210| 3|x10-012210| 4|110--01x10| 5|110---1110| 6|x100000000| 7|1223210---| 8|01xxx10---| 9|0123210---| ==========