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


A問題(50点)

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

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

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

typedef struct{
	int id;           /* ID */
	char name[12];    /* 名前 */
	int year;        /* 学年 */
	char subject[12]; /* 科目名 */
	char grade;      /* 成績 */
}StudentInfo;
A. 構造体を定義・初期化し、関数を使って表示する。
(提出ファイル名 prog01a.c)
  1. 定義された構造体に対し、下の実行例を参考にして二人分の学生のデータ(実行例以外の適当な架空のデータを用いてもよい)で、 student1student2を初期化せよ。
  2. /* 以下の構造体の初期化はmain()の中に書くこと */
    StudentInfo student1 = {学生1のデータ};
    StudentInfo student2 = {学生2のデータ};
    
  3. 構造体の5つのメンバ変数の内容を出力する関数を書け。 ただし、関数のプロトタイプは次のようなものであるとする:
  4. void PrintInfo(StudentInfo);
    
  5. 構造体のポインタを使用し、上と同様に構造体の内容を出力する関数を追加せよ。 関数のプロトタイプは次のようなものであるとする:
  6. void PrintInfoPtr(StudentInfo *);
    
  7. 以上の2つの関数の動作を確認するために、 両方の関数を使用して student1student2の内容を出力するようにせよ。 また、それぞれの関数での出力が同じであることを確認せよ。
実行例:
% ./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.cprog01b.cにコピーし、 構造体の宣言はそのままに、構造体配列を使ったものに変更する。
(提出ファイル名 prog01b.c)
  1. 初期値として、下の例を参考にして二人分の学生のデータを使用して、 構造体配列studentdataを初期化する。
  2. /* 構造体配列をmain()中で以下のように初期化する */
    StudentInfo studentdata[2] = {
    	{学生1のデータ},
    	{学生2のデータ}
    };
    
  3. この構造体配列を、PrintInfo(), PrintInfoPtr()を使って表示しなさい。 関数はprog01a.cのものをそのまま使用する。
C. さらにprog01b.cprog01c.cにコピーした上、 構造体配列の要素数を増やし、入力用関数でデータを読み込み、表示する。
(提出ファイル名: prog01c.c)
  1. 入力用関数InputInfo()を追加する。 この関数は、一人分のデータを読み込む関数として実装すること。さらに、 関数のプロトタイプは次のようなものであるとし、戻り値はscanfの戻り値に準拠すること:
  2. int InputInfo(StudentInfo *);
    
  3. データはdata.txt/home/course/prog1/public_html/2024/ex/ex08/data.txtにファイルがあるのでコピーすること)からリダイレクションで読み込むものとする。 ただし、最大データ数は20として、マクロNで設定すること。
  4. この構造体配列を、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(構造体の入れ子構造)

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

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

typedef struct{
    XY center;
    double radius;
    double length;
    double area;
}CIRCLE; /* centerを中心点、radiusを半径とする円。lengthは周長、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
  Length, Area : 12.566371, 12.566371

構造体ポインタを使う関数 (inputCirclePtr):
円の中心点の座標と円周上の1点の座標を入力:
0.0 0.0 2.0 0.0
Circle2:
  Center, Radius : (0.000000, 0.000000), 2.000000
  Length, Area : 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
  Length, Area : 6.283185, 3.141593

構造体ポインタを使う関数 (inputCirclePtr):
円の中心点の座標と円周上の1点の座標を入力:
-2.0 -2.0 -2.0 -3.0
Circle2:
  Center, Radius : (-2.000000, -2.000000), 1.000000
  Length, Area : 6.283185, 3.141593

本プログラムにおいて、math.hをインクルードすることで, 円周率を表すマクロM_PIや、平方根を計算するsqrt関数を使用できる。
math.hをインクルードする場合,コンパイル時にはgcc -lm prog02.cのように -lmオプション指定を加えるのを忘れないこと。

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

乱数で0から5までの整数値をN(マクロで定義する)回発生させ、 各出目の出現頻度を調べるプログラムを作成する。 これらのデータは以下の構造体に格納するものとする。指示に従って順を追ってプログラムを完成させよ。
(提出ファイル名 prog03a.c, prog03b.c, prog03c.c)
#define N 600000 /* 試行回数 */

typedef struct{
    int data[N]; /* N個の要素を持つ配列 */
    int count[6]; /* 各出目の回数 */
}DiceRollData;
  1. 提出ファイル名 prog03a.cの作成
    • 乱数の生成
      1. このプログラムでは乱数を使用するが、毎回違う乱数系列を作るために、 次のようにsrand関数を呼び出して乱数の初期値として time関数を用いて得た現在時刻を与える (乱数の初期値を与えることを「乱数の種を作る」とも言う。 これはrand関数で乱数を作る前に一回だけ行えばよい)。
      2. srand((unsigned int)time(NULL));
        

        注意:time関数の使用の際にはtime.hを、 またsrand関数とrand関数を用いる際には stdlib.hをインクルードする必要がある。

      3. rand関数は0からマクロRAND_MAXまでの範囲の 整数値の乱数(正確には疑似乱数である。問題文の下の注記を参照のこと)を返すので、 以下のようにすれば0から5までの整数値の乱数を生成できる。 これをN回繰り返して、DiceRollData型構造体の変数 (変数名は任意)のメンバである配列data[N]に格納する。
      4. rand() % 6;
        
      5. 乱数を格納したDiceRollData型構造体の変数のメンバである 配列count[6]をすべて0で初期化する。
    • ポインタ渡しの関数による構造体の操作
      1. DiceRollData型構造体の変数に格納された乱数の各出目の出現頻度を調べ、 メンバの配列count[6]に格納する関数ComputeStatisticsPtrを作成せよ。 関数のプロトタイプは以下のようなものであるとする:
      2. void ComputeStatisticsPtr(DiceRollData*);
        
      3. 関数ComputeStatisticsPtrを呼び出して、出目の出現頻度を調べ、その結果を main関数内で表示せよ。
      4. 実行例(乱数によるプログラムなので、表示される数値は毎回異なる):

        % ./a.out
        [0]: 100468
        [1]: 99540
        [2]: 100101
        [3]: 100352
        [4]: 99671
        [5]: 99868
        

        実行して得られる出目の出現頻度の偏りは、試行回数Nが少なければ大きくなり、多くすれば小さくなる。ただし、自動変数として宣言可能な配列変数、構造体変数のサイズには上限(環境によって異なる)があるので、Nを極端に大きくするとコンパイルに失敗したり、実行時にエラーが発生することがある。第6回で学んだ動的メモリ割り当てを使うと、この制限を超えることができる。

  2. 値渡しの関数による構造体の操作(提出ファイル名 prog03b.c
    • prog03a.cprog03b.cにコピーして、以降はprog03b.cを編集すること。
    • 構造体を関数の戻り値として扱うこともできる。構造体変数の計算結果を戻すことで、 関数の呼び出し元にその構造体を値渡しで戻すことができる。
      これを利用し、ComputeStatisticsPtr関数を元に次のような関数を作成し、 こちらを用いても同様に動作することを確認せよ(ComputeStatisticsPtr関数は消さずにプログラム中に残しておくこと)。
    • DiceRollData ComputeStatisticsV(DiceRollData);
      

      実行例は省略

  3. 値渡しの関数による構造体の操作と実行時間の計測(提出ファイル名 prog03c.c
    • prog03b.cprog03c.cにコピーして、以降はprog03c.cを編集すること。
    • 最後に、ポインタ渡しの関数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問題

問題4(問題2の拡張)

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

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

なお、問題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
%

問題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---|
  ==========