プログラミングC
第8回 演習問題


A問題(50点)

問題1(構造体引数とアロー演算子の使い方)

構造体を関数に渡す方法とアロー演算子の使い方について確認する。

以下のような構造体を宣言し、指示に従ってプログラムを作成せよ。 (提出ファイル名 prog01a.c, prog01b.c, prog01c.c)

typedef struct{
	int id;           /* ID */
	char name[10];    /* 名前 */
	int grade;        /* 学年 */
	char subject[10]; /* 科目名 */
	char record;      /* 成績 */
}StudentInfo;
A. 構造体を定義・初期化し、関数を使って表示する。
(提出ファイル名 prog01a.c)
  1. 定義された構造体に対し、あなたのデータと友人のデータ(データは適宜作成せよ)を用いて、 student1student2を初期化せよ。
  2. /* 以下の構造体の初期化はmain()の中に書くこと */
    StudentInfo student1 = {あなたのデータ};
    StudentInfo student2 = {友人のデータ};
    
  3. 構造体の5つのメンバ変数の内容を出力する関数を書け。 ただし、関数のプロトタイプは次のようなものであるとする:
  4. void PrintInfo1(StudentInfo);
    
  5. 構造体のポインタを使用し、上と同様に構造体の内容を出力する関数を追加せよ。 関数のプロトタイプは次のようなものであるとする:
  6. void PrintInfo2(StudentInfo *);
    
  7. 以上の2つの関数の動作を確認するために、 両方の関数を使用して student1student2の内容を出力するようにせよ。 また、それぞれの関数での出力が同じであることを確認せよ。
実行例:
% ./a.out
構造体の値渡し
202101     Haruto 2    English A
202102        Mio 3       Math B
構造体のアドレス値渡し
202101     Haruto 2    English A
202102        Mio 3       Math B
%
B. 次にprog01a.cprog01b.cにコピーし、 構造体の宣言はそのままに、構造体配列を使ったものに変更する。
(提出ファイル名 prog01b.c)
  1. 初期値として、あなたのデータと友人のデータ(データは構造体の内容に沿うよう、適宜作成せよ)を使用して、 構造体配列dataを初期化する。
  2. /* 構造体配列をmain()中で以下のように初期化する */
    StudentInfo data[2] = {
    	{あなたのデータ},
    	{友人のデータ}
    };
    
  3. この構造体配列を、PrintInfo1(), PrintInfo2()を使って表示しなさい。 関数はprog01a.cのものをそのまま使用する。
C. さらにprog01b.cprog01c.cにコピーした上、 構造体配列の要素数を増やし、入力用関数でデータを読み込み、表示する。
(提出ファイル名: prog01c.c)
  1. 入力用関数InputData()を追加する。 このInputData()は、一人分のデータを読み込む関数として実装すること。さらに、 関数のプロトタイプは次のようなものであるとし、戻り値はscanfの戻り値に準拠すること:
  2. int InputData(StudentInfo *);
    
  3. データはdata.txtからリダイレクションで読み込むものとする。 ただし、最大データ数は20として、マクロNで設定すること。
  4. この構造体配列を、PrintInfo1(), PrintInfo2()を使って表示しなさい。 関数はprog01a.cのものをそのまま使用する。

data.txtは,データ区切りに空白を使用している. scanf使用時で入力フォーマット指定子の%cを使用した場合,スペースや改行等も一文字として読み込もうとするため, 入力フォーマット指定子%cの前に一つスペースを入れてデータ区切りの空白を読み飛ばすように工夫をすること.

実行例:
% ./a.out < data.txt
構造体の値渡し
202111       Sota 2       Math A
202112        Mei 3    English A
202113     Minato 1        Art C
202114     Ichika 4      Music C
202115     Haruki 3   Japanese B
構造体のアドレス値渡し
202111       Sota 2       Math A
202112        Mei 3    English A
202113     Minato 1        Art C
202114     Ichika 4      Music C
202115     Haruki 3   Japanese B

B問題(50点)

問題2(構造体の入れ子構造)

以下のように、2次元平面上の点を表すためにXY型の構造体と それを使って円を表すためのCIRCLE型構造体を宣言した。 CIRCLE型構造体は中心点をXY型構造体, 円周上の1点をXY型構造体、半径をdouble型の変数で表現している。

typedef struct{
	double x; /* x座標 */
	double y; /* y座標 */
}XY; /* 平面上の点 */

typedef struct{
	XY center;
	XY p;
	double r;
}CIRCLE; /* centerを中心点、pを円周上の点、rを半径とする円 */

この構造体を使用し、2次元平面上の円の中心点および円周上の1点の座標を入力し、 円の周長と円の面積を求めるプログラムを作成しなさい。

ただし、データの入力処理については、以下のプロトタイプにあるように、 値の受渡し方法が異なる2種類の関数を作成して用いることする。 また、この関数内で半径の値を計算して、メンバ変数rにその値を代入すること。 そして、実行例にあるように動作確認を行う。(必要に応じて関数を追加しても良い。)
(提出ファイル名 prog02.c)

CIRCLE input1(void);   /* データを読み込んだ構造体を戻す */
void input2(CIRCLE *); /* 構造体のポインタを渡し、そこにデータを読み込む */
実行例:
% ./a.out
データの入力(構造体を返す関数):
0.0 0.0 0.0 2.0
input1: length, area : 12.566371, 12.566371
データの入力(構造体ポインタを使う関数):
0.0 0.0 0.0 2.0
input2: length, area : 12.566371, 12.566371
% ./a.out
データの入力(構造体を返す関数):
-2.0 -2.0 -2.0 -3.0
input1: length, area : 6.283185, 3.141593
データの入力(構造体ポインタを使う関数):
1.0 1.0 2.0 1.0
input2: length, area : 6.283185, 3.141593

本プログラムにおいて、math.hをインクルードすることで, 円周率を表すM_PIを使用することができる。
また、math.hをインクルードすることで,平方根を計算するsqrtを使用できる。
math.hをインクルード場合,コンパイル時の-lmオプション指定を忘れないこと。

問題3(構造体引数と構造体ポインタ引数)

乱数で0から999までの整数値をN(マクロで定義する)回発生させ、 その最大値、最小値、平均値を求めるプログラムを作成する。 これらのデータは以下の構造体に格納するものとする。指示に従って順を追ってプログラムを完成させよ。
(提出ファイル名 prog03a.c, prog03b.c)
#define N 90000
typedef struct{
	int data[N]; /* N個の要素を持つ配列 */
	int max;     /* データの最大値 */
	int min;     /* データの最小値 */
	double ave;  /* データの平均値 */
}My_Array;

/* 以下の構造体変数の宣言はmain()関数の中に書くこと */
My_Array A;
  1. 提出ファイル名 prog03a.cの作成
    • 乱数の生成
      1. このプログラムでは乱数を使用するが、毎回違う乱数系列を作るために、 次のように乱数の初期値に現在時刻を与える (乱数の初期値を与えることを「乱数の種を作る」とも言う。 また、これはrand()を呼ぶ前に一回だけ行う)。
      2. srand((unsigned int)time(NULL));
        

        注意:上記の式を使う場合はtime.hstdlib.hをインクルードせよ。

      3. 乱数を発生させる関数rand()を使用して、 (main()関数内で)構造体Adataの要素全てに、 0から999までの乱数を代入するプログラムを作成せよ。
      4. A.data[i] = rand() % 1000;
        
    • 構造体の操作
      1. 構造体Adataの最大値、最小値、平均値を求め、 対応するメンバに代入する関数を作成せよ。関数のプロトタイプは以下のようなものであるとする:
      2. void FindMember1(My_Array);
        
      3. 求めた最大値、最小値、平均値を対応するメンバ変数に代入した後に、 各メンバ変数の値をFindmember1関数内で出力し, main関数内で最大値、最小値、平均値に対応するメンバ変数を出力せよ。
      4. しかしながら、Findmember1関数内で求めた値が main関数に反映されない。(下の実行例を参照)
      5. 実行例(乱数によるプログラムなので、表示される数値は毎回異なる):

        % ./a.out
        FindMember1
        Maximum value: 999
        Minimum value: 0
        Average value: 499.725411
        main
        Maximum value: 60624936
        Minimum value: 1
        Average value: 0.000000
        
      6. main関数に反映されない理由を理解した上で、関数のプロトタイプを次のように変更し、 関数の内容も修正し、プログラムを実行して題意を満たしているか確認せよ。
      7. void FindMember1(My_Array *);
        

        実行例(乱数によるプログラムなので、表示される数値は毎回異なる):

        % ./a.out
        FindMember1
        Maximum value: 999
        Minimum value: 0
        Average value: 498.473700
        main
        Maximum value: 999
        Minimum value: 0
        Average value: 498.473700
        
  2. 構造体を返す関数と実行時間の計測(提出ファイル名 prog03b.c
    • 構造体を関数の戻り値として扱うこともできる。構造体変数の計算結果をリターンすれば、 関数を呼ぶ側にその構造体を渡して正しい結果が出力できる。
      これを利用し、FindMember1()を元に次のような関数を作成し、 うまく動作することを確認せよ(FindMember1()は消さずにプログラム中に残しておくこと)。
    • My_Array FindMember2(My_Array);
      
    • lec08-20を参考に、 FindMember1FindMember2それぞれの実行時間を計測して計測結果を出力せよ。
      (実行例通りになるようにprog03b.cの中ではFindMember1()を変更して,値の出力部の箇所は時間計測の対象にしないこと)
    • ループ回数を30000とした実行例(乱数によるプログラムなので、表示される数値は毎回異なる):

      % ./a.out
      FindMember1
      Maximum value: 999
      Minimum value: 0
      Average value: 500.435922
      
      FindMember2
      Maximum value: 999
      Minimum value: 0
      Average value: 500.435922
      
      --- time ---
      FindMember1: 12.088380 sec
      FindMember2: 13.014098 sec
      

本プログラムにおいて、最適化オプション-O2などを付けずに、 プログラムをコンパイルすること。

1回の実行時間のみを計測すると、実行時間が短すぎて上手く計測できない。 それぞれの関数呼び出しをループを用いて10000回繰り返してみること。 ただし、実行時間が長すぎたり、短すぎる場合はループ回数を適宜変更してみても良い
(Macの場合、30000回ループぐらいが適当)

参考: C言語標準のrand関数、srand関数を用いた乱数の発生は簡便ではあるが、 得られる乱数の質はあまり良くない。実用的には他の方法で乱数を発生させることがよく行われるので、 興味がある人はメルセンヌ・ツイスタなど調べてみるとよい。

Extra問題

問題4(問題2の拡張)

問題2では構造体CIRCLEを利用して円を表現したが、 それに高さを加えたCYLINDER型の構造体を作成し、 底面の円の円周長、面積、円柱の表面積および体積を計算し表示するプログラムを作成する。

(提出ファイル名 prog04.c)

なお、先の入力関数を、円柱データに対応するように修正するものとする (底面の円の半径の値を計算し,メンバ変数にその値を代入する処理も行うこと)。 また、以下に示す構造体CYLINDERを追加する:

typedef struct{
	CIRCLE circle;
	double h;
}CYLINDER;
実行例:
% ./a.out
データを入力して下さい(構造体を返す関数):
円柱底面の円の中心座標(x, y): 0.0 0.0
円柱底面の円周上の1点の座標(x, y): 1.0 0.0
円柱の高さh: 1.0
input1: length, area : 6.283185, 3.141593
input1: surface, volume : 12.566371, 3.141593
データを入力して下さい(構造体ポインタを使う関数):
円柱底面の円の中心座標(x, y): 0.0 0.0
円柱底面の円周上の1点の座標(x, y): 1.0 0.0
円柱の高さh: 1.0
input2: length, area : 6.283185, 3.141593
input2: surface, volume : 12.566371, 3.141593
% ./a.out
データを入力して下さい(構造体を返す関数):
円柱底面の円の中心座標(x, y): 1.0 1.0
円柱底面の円周上の1点の座標(x, y): 3.0 1.0
円柱の高さh: 2.0
input1: length, area : 12.566371, 12.566371
input1: surface, volume : 50.265482, 25.132741
データを入力して下さい(構造体ポインタを使う関数):
円柱底面の円の中心座標(x, y): 1.0 1.0
円柱底面の円周上の1点の座標(x, y): 1.0 3.0
円柱の高さh: 2.0
input2: length, area : 12.566371, 12.566371
input2: surface, volume : 50.265482, 25.132741

問題5(マインスイーパー)

構造体の宣言と定義を以下のように行う時、以下の指示に従って順を追ってプログラムを作成せよ。
(提出ファイル名: prog05.c)
#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マスの中で開けたマスの周りのマスにいくつ地雷があるかを数で表示します。 開けたマスに地雷があれば、ゲーム終了となります。 旗を置く操作を行うと、そのマスに旗が置かれます。 すべての地雷に旗を置くと終了となりますが、地雷のない場所に旗が置いてあると、 ゲーム終了とはなりません。以上を含んだゲームの仕様を示します。

  1. 盤面は10x10とする。
  2. 地雷はランダムに5~15個設置する。
  3. 全ての地雷に旗を置くことでクリアとする。※地雷以外に旗を置いた場合はクリアとならない。
  4. 地雷を1つでも開けるとゲーム終了とする。
  5. 座標(x, y)を入力し、開く箇所と旗を置く箇所を指定する。
  6. 開くか旗を置くかは座標を入力する前に選択する。
  7. 入力した座標のみ開くこととする。
  8. 旗が置いてある箇所は開くことは出来ない。
  9. 開いている箇所に旗を置くことは出来ない。
  10. 再度旗を置くことで旗を削除する。
  11. クリア、またはゲーム終了の際には盤面の地雷ある箇所を全て見せる。
template05.c内のコメントと下記を参考にプログラムを完成させてください。
また、必要に応じて変数を追加すること。
  1. void Initialize(Map*)関数
    構造体Mapの初期化を行う。 設置する地雷の数、地雷の座標を乱数を用いて決めること。
    ただし、地雷の座標を乱数で決めたときに、 同じ座標に複数の地雷が重なってしまう場合がある。
    プログラムを工夫し、重ならないようにすること。
  2. int Judge(Map*)関数
    盤面上のの開くマス、旗を置くマスを入力して、ゲームを続行できるかを判定する。
    指定するマスを開くか旗を置くかについては最初に0か1を入力することで判別すること。 また-1が入力された時は終了することとする。
    旗の総数と正しく置いた旗の数、地雷の数が等しいとき、クリアとなる。
  3. void View(Map)関数
    盤面をコンソール(端末)上に表示する。
    それぞれのマスに対し、初期状態を[-]、旗を置いた状態を[F]、 開いている状態は周辺の地雷の数を表示する。 また、開いている箇所に地雷がある場合は[x]とする。 詳細な表示形式は、実行例を参考にすること。
  4. 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---|
  ==========