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


すべての問題について、特別の指示が無い限り外部変数は使用しないで下さい。

A問題(50点)

問題1(マクロ関数)

下記のマクロ関数を使ったプログラムは、引数の与え方によって正しく動作しない場合がある。

#include <stdio.h>

#define func(x)  x*x-2

int main()
{
  int a = 2;
  double b = 1.5;

  printf("a=%d, b=%f\n", a, b);
  printf("func(a) = %d\n", func(a));
  printf("func(a+2) = %d\n", func(a + 2));
  printf("func(a)*4 = %d\n", func(a) * 4);
  printf("func(b) = %f\n", func(b));
  printf("func(b+2.5) = %f\n", func(b + 2.5));

  return 0;
}
実行結果(一部の結果は正しくない)
% ./a.out
a=2, b=1.500000
func(a) = 2
func(a+2) = 6正しくない
func(a)*4 = -4正しくない
func(b) = 0.250000
func(b+2.5) = 5.750000正しくない
  1. 上記のプログラムでは、func(a)a*a-2 に置き換えられる。 func(a+2)func(a)*4で正しくない実行結果が出力される理由を理解するために、 func(a+2)とfunc(a)*4の置き換え結果がどのようになるのかを考えて prog01.txt に記述しなさい (書式は以下に示すものを使うこと)。
  2. プログラムが正しく動作するように、マクロの定義のみを修正し、 prog01.c というファイル名で保存しなさい。 ただし、引数に自己の値を変更するような式(インクリメント、デクリメント、i=i+2のような式) は含まれないと仮定してよい。
  3. 修正後のマクロ定義でfunc(a)func(a+2)func(a)*4の 置き換え結果も prog01.txt に記述しなさい。
    prog01.txtの全体の記述内容は以下の通りである。下線部を記入すること。
      修正前 #define func(x)  x*x-2の場合
      func(a)の置き換え結果: a*a-2
      func(a+2)の置き換え結果: ________________
      func(a)*4の置き換え結果: ________________
      
      修正後 #define func(x)  ________________ の場合
      func(a)の置き換え結果:   ________________
      func(a+2)の置き換え結果: ________________
      func(a)*4の置き換え結果: ________________  
      
(提出ファイル名:prog01.txt, prog01.c)

正しい実行例
% ./a.out
a=2, b=1.500000
func(a) = 2
func(a+2) = 14
func(a)*4 = 8
func(b) = 0.250000
func(b+2.5) = 14.000000

問題2(条件コンパイル)

問題の前に、講義Lec12-12~16 (p140~141)で学んだこと(条件コンパイル)の確認を行う。 こちらのプログラムcompile_sample.c は、キーボードから奇数を入力すると、 その数以下の正の奇数の和を計算して表示するプログラムである。 (例えば、9 を入力すると 1 + 3 + 5 + 7 + 9 = 25 を表示する)
このプログラムをファイル名compile_sample.cとして保存し、以下の4通りのやり方でコンパイルして実行し、動作の違いを確かめなさい。

  1. gcc でそのままコンパイルして実行する
  2. gcc-DDEBUG というオプションを付けてコンパイルして実行する
  3. プログラム中のマクロ JAPANESE の値を 0 に変えて保存し、コンパイルして実行する
  4. プログラム中のマクロ JAPANESE の値を 0 に変えて保存し、さらに -DDEBUG オプションを付けてコンパイルして実行する
コンパイル・実行例
1の場合
% gcc compile_sample.c
% ./a.out
奇数を入力してください: 9
1 から 9 までの奇数の和 = 25

2の場合
% gcc -DDEBUG compile_sample.c 
% ./a.out
奇数を入力してください: 9
i = 1,  sum = 1
i = 3,  sum = 4
i = 5,  sum = 9
i = 7,  sum = 16
i = 9,  sum = 25
1 から 9 までの奇数の和 = 25

3の場合(マクロの値を変えた後)
% gcc compile_sample.c
% ./a.out
Please input an odd number: 9
Sum of odd numbers from 1 to 9 = 25

4の場合(マクロの値を変えた後)
% gcc -DDEBUG compile_sample.c 
% ./a.out
Please input an odd number: 9
i = 1,  sum = 1
i = 3,  sum = 4
i = 5,  sum = 9
i = 7,  sum = 16
i = 9,  sum = 25
Sum of odd numbers from 1 to 9 = 25

他の3つの場合も同じように試し、それぞれの表示結果とプログラムを見比べ、 コンパイル時にプログラムのどの部分が有効になっているか確認すること。
#if, #ifdef, #else 等の機能を理解できたら、以下の問題に進みなさい。


プログラム lec12-6a.clec12-6b.c とヘッダファイル lec12-4.h は第12回のハンドアウト(p142~143)に出てきた分割コンパイルの例である。 まず、各ファイルを

  1. ヘッダファイルは、 prog02header.h
  2. メイン試験用プログラムは、prog02main.c
  3. calcarea試験用プログラムは、prog02calc.c
という名前で自分のディレクトリにコピーした上で、以下の課題を行いなさい。

  1. まず、ファイル名を変えてコピーしたことに伴い、プログラム中に出てくるファイル名も、 変更が必要なものは変更しなさい。
  2. prog02main.cprog02calc.c では、単体テスト用に追加された部分が常に有効になっているため、これらを結合して 全体の動作確認を行うことができない (両方一緒にコンパイルすると、同じ名前の関数が二つあるのでエラーになる)。 そこで、単体テスト用の部分が、コンパイル時に-DTESTというオプションを付けた時のみ有効 になるように修正して、単体テストと最終的な実行ファイルによる結合テストが 同じソースコードのままで行えるように書き改めなさい。
  3. 書き改めたソースコードで、単体テストと結合テスト(最終的な実行ファイルの動作確認)を行う 際のそれぞれのコンパイル方法をprog02.txt に書きなさい。なお、 結合テスト(最終的な実行ファイルの動作確認)向けのコンパイル方法は、複数コマンドで順次行う場合と 一度に行う場合の2通りが考えられる。両方を記載すること。 prog02.txtの全体の記述内容は以下の通りである。下線部を記入すること。
      単体テスト
      prog02main.c: ________________
      prog02calc.c: ________________
      
      結合テスト/最終的な実行ファイルの動作確認
      複数コマンドで順次行う場合:
      ________________
      ________________
      ________________
      
      一度に行う場合: ________________
      

(解答ファイル名:prog02header.h, prog02main.c, prog02calc.c, prog02.txt)

実行例(prog02main.cの単体テスト)
% ./a.out
0 0 0 1 1 0
0 0
0 1
1 0
Area = 0.000000
実行例(prog02calc.cの単体テスト)
% ./a.out
Area = 0.500000
実行例(結合テスト/最終的な実行ファイルの動作確認)
% ./a.out
0 0 0 1 1 0
Area = 0.500000
% ./a.out
1 0 2 0 3 3
Area = 1.500000
マクロ定義による条件コンパイルを行う場合のマクロの名称はハンドアウトやこの問題の例題でのDEBUGで 固定されているわけではなく、任意に選ぶことができる。この問題では単体テスト用のコンパイルで-DTEST というオプションが指定されているので、マクロ名としてはTESTとなる。このほか各問題ごとにマクロ名が 指定されていればそれに従うこと。

B問題(50点)

問題3(分割コンパイルによるプログラム開発)

以下のプログラム prog03main.c は、関数 read_planets によって太陽系の惑星のデータ planet.txt を Planet型の構造体配列 planets[] に読み込み、表示を行うためのものである。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "prog03header.h"

int main() {
  int i;
  Planet planets[NPLANETS];
  
  if( read_planets( planets ) != 0 ){
      fprintf(stderr, "Input Error!\n");
      exit(3);
  }

  for (i = 0; i < NPLANETS; i++) {
    printf("Name: %s\n", planets[i].name);
    printf("  Radius: %.0f [km]\n", planets[i].radius);          /* 半径 */
    printf("  Density: %.2f [g/cm3]\n", planets[i].density);     /* 密度 */
    printf("  Semi-Major Axis: %.3e [km]\n", planets[i].semi_major_axis);   /* 軌道長半径 */
  }

  return 0;
}

#ifdef TEST0 /* 単体テスト用 read_planets 正常動作(戻り値 0 )の場合 */
int read_planets(Planet *planets) {
  int i;
  for (i = 0; i < NPLANETS; i++) {
    strcpy(planets[i].name, "Sample");     /* テスト用のサンプルデータ */
    planets[i].radius = 1000;
    planets[i].density = 1.00;
    planets[i].semi_major_axis = 100000000;
  }
  return 0;
}
#elif TEST1 /* 単体テスト用 read_planets 異常動作(戻り値 -1 )の場合 */
int read_planets(Planet *planets) {
  return -1;
}
#endif
正しくデータを読み込めたときに想定される実行結果:
Name: Mercury
  Radius: 2440 [km]
  Density: 5.43 [g/cm3]
  Semi-Major Axis: 5.790e+07 [km]
Name: Venus
  Radius: 6052 [km]
  Density: 5.24 [g/cm3]
  Semi-Major Axis: 1.082e+08 [km]
Name: Earth
  Radius: 6378 [km]
  Density: 5.52 [g/cm3]
  Semi-Major Axis: 1.496e+08 [km]
Name: Mars
  Radius: 3396 [km]
  Density: 3.93 [g/cm3]
  Semi-Major Axis: 2.279e+08 [km]
Name: Jupiter
  Radius: 71492 [km]
  Density: 1.33 [g/cm3]
  Semi-Major Axis: 7.783e+08 [km]
Name: Saturn
  Radius: 60268 [km]
  Density: 0.69 [g/cm3]
  Semi-Major Axis: 1.429e+09 [km]
Name: Uranus
  Radius: 25559 [km]
  Density: 1.27 [g/cm3]
  Semi-Major Axis: 2.875e+09 [km]
Name: Neptune
  Radius: 24764 [km]
  Density: 1.64 [g/cm3]
  Semi-Major Axis: 4.504e+09 [km]

しかし、関数 read_planets はまだ実装されておらず、 代わりに単体テスト用のダミーのコード(#ifdef 以降の部分; 実際に読み込みはしていない)が prog03main.c の末尾に付加されている。

0.(準備)
まず、ファイル prog03main.cprog03header.hplanet.txt を自分の作業ディレクトリにコピーしておくこと。

1.
コンパイル時のマクロ定義を利用して、prog03main.c 単体で動作テストを行う際のコンパイル方法を prog03.txt に書け。その際、関数 read_planets戻り値が 0 の場合(正常)と -1 の場合(異常)の二通りの動作 を確認する方法を調べること。

プログラム中で使われている #elif などの条件コンパイルのための マクロ・プリプロセッサについては、ハンドアウト Lec12-12, 13や C言語入門講座 9.プリプロセッサとメイクファイル などを参考にすること。

コンパイルできた時の実行例(関数read_planetsの戻り値 0 の場合)
% ./a.out
Name: Sample
  Radius: 1000 [km]
  Density: 1.00 [g/cm3]
  Semi-Major Axis: 1.000e+08 [km]
  (以下同じ出力の繰り返し)
コンパイルできた時の実行例(関数read_planetsの戻り値 -1 の場合)
% ./a.out
Input Error!
%

2.
prog03read.c に、下記の仕様に基づいてファイルからデータ読み込みを行う正規の関数 read_planets を作成せよ。 さらに、prog03main.c と prog03read.c を組み合わせて全体での動作テストを行う際のコンパイル方法 (複数コマンドで順次行う場合と一度に行う場合の2通りを prog03.txt に書け

なお、prog03main.c と prog03header.h の内容を変更してはならない。

prog03.txtの全体の記述内容は以下の通りである。下線部を記入すること。
prog03main.c単体の動作テスト
戻り値が 0 の場合(正常): ________________
戻り値が -1 の場合(異常): ________________

prog03main.c と prog03read.cを組み合わせた結合テスト/最終的な実行ファイルの動作確認
複数コマンドで順次行う場合:
________________
________________
________________
  
一度に行う場合: ________________
(解答ファイル名:prog03read.c, prog03.txt

Extra問題

問題4

以下のプログラムでは、2つの三角形についてそれぞれの3頂点の平面座標を キーボードから入力して構造体に格納し、面積を求める。ただし面積が 0 の場合は 3点が三角形を成していないので、再度頂点の座標の入力を促す。
三角形2つの座標が入力できたら、どちらの三角形の面積が大きいか判定して表示し、 さらに2つの三角形それぞれの重心を求めて表示する。 この未完成のプログラムを下記の指示にしたがって完成させよ。
#include <stdio.h>
#include "prog04header.h"     /* (1) */
/* 必要に応じて追加、修正してもよい */

int main() {     /* (2) */
  /* 必要に応じてコードを追加 */

  /* 1つ目の三角形の3頂点の座標を入力する */
  InputTriangle( );     /* 引数・戻り値は適宜設計せよ (3) */
  /* 1つ目の三角形の面積を求める */
  CalcArea( );     /* 引数・戻り値を入れる変数等は適宜設計せよ (4) */
  
  /* CalcAreaの結果をもとに、先へ進むか,再入力を求める */
  
  /* 2つ目の三角形の3頂点の座標を入力する */
  InputTriangle( );   /* 引数・戻り値は適宜設計せよ (3) */
  /* 2つ目の三角形の面積を求める */
  CalcArea( );   /* 引数・戻り値を入れる変数等は適宜設計せよ (4) */

  /* CalcAreaの結果をもとに、先へ進むか,再入力を求める */
  

  printf( /* CalcAreaで求めた三角形の面積を2つ表示 */ );

  /* 面積の大きい方を判定して、表示 */

  /* 1つ目の三角形の重心を求める */
  CalcCentroid( );   /* 引数・戻り値等は適宜設計せよ (5) */
  /* 2つ目の三角形の重心を求める */
  CalcCentroid( );   /* 引数・戻り値等は適宜設計せよ (5) */
  
  printf( /* CalcCentroidで求めた三角形の重心2つを表示 */ );

  return 0;
}
実行例(出力内容を厳密にこの例に合わせる必要はない)
% ./a.out
Input 1st triangle:
Vertex 1: 2.0 2.0
Vertex 2: 2.0 8.0
Vertex 3: 2.0 12.0
These points do not form a triangle!
Again, input 1st triangle:
Vertex 1: 1.0 3.0
Vertex 2: 4.0 9.0
Vertex 3: 7.0 2.0
Input 2nd triangle:
Vertex 1: 0.0 0.0
Vertex 2: 4.0 0.0
Vertex 3: 2.0 9.0

Area of 1st one: 19.500   Area of 2nd one: 18.000
The 1st one is larger than the 2nd one!

Centroid of 1st one:  (4.000, 4.667)
Centroid of 2nd one:  (2.000, 3.000)
必要な作業は以下の通りである。
(1)
必要な構造体宣言、関数のプロトタイプ宣言を含むヘッダファイル prog04header.h の作成
(2)
上記の main関数を含むプログラム本体 prog04_2.c の完成。 (上記未完成プログラムに含まれている関数の削除や外部変数の使用は禁止する。 関数の実行位置の変更や引数、戻り値は任意に決めてよい。 また、必要な変数・if や for などの制御文の追加、他の関数の追加も自由にしてよい)
(3)
3頂点の平面座標をキーボード入力から受け取り構造体に格納する関数 InputTriangle の作成(prog04_3.c
(4)
三角形の面積を求める関数 CalcArea の作成(prog04_4.c
(5)
三角形の重心を求める関数 CalcCentroid の作成(prog04_5.c
(6)
このプログラムの実行可能形式ファイルを作成するために必要なコンパイル、リンクの手順を 簡潔に記述する prog04.txt の作成

(解答ファイル名:prog04header.h, prog04_2.c, prog04_3.c, prog04_4.c, prog04_5.c, prog04.txt)