レポート課題
これまで学んできたテクニックを使って、簡単な「数値データ処理プログラム」を作ります。
最終的な目標は、以下のような機能を持つプログラムにすることです。
2.データファイルからの読み込み、処理結果のファイルへの書き出し
目指すプログラムの概要はこちら。
それぞれの内容についてはリンク先を参照して下さい。
必要と思われるところではプログラム例を示してありますが、必ずしもそれに従って書く必要はありません。むしろ自分自身で工夫したプログラムのほうが、自ずと評価は高くなります。
レポート提出方法などについてはこちらで確認して下さい。
0 準備
大きなプログラムの書き進め方には「トップ・ダウン」式と「ボトム・アップ」式があります。前者はプログラムの大枠を始めにつくり、あとから細かい部分を作っていく方式で、後者は個々の細かい機能のプログラムを作ってから、全体をまとめる方式です。
ここではトップ・ダウン式で進めます。
0.1 プログラムの構造
0.1.1 プログラムの全体構造
全体の動作を規定する構造として、例えば以下のようなプログラムを作っておきます。
#include
<stdio.h>
#define
LOAD 1
#define
SAVE 2
#define
PROCESS 3
#define
DISPLAY 4
#define
ABORT 5
void
do_load(void);
void
do_save(void);
void
do_process(void);
void
do_display(void);
void
do_abort(void);
int main(void) {
int op;
do
{
/*
行う操作をキーボードから入力 */
printf("Select operation:\n");
printf(" %d: Load\n", LOAD);
printf(" %d: Save\n", SAVE);
printf(" %d: Process\n", PROCESS);
printf(" %d: Display\n", DISPLAY);
printf(" %d: Abort\n", ABORT);
scanf("%d", &op);
/*
入力された値に従って、各処理へと分岐 */
switch
(op) {
case
LOAD: do_load(); /*
ファイルからのデータの読み込み */
break;
case
SAVE: do_save(); /*
ファイルへの演算結果の書き込み */
break;
case
PROCESS: do_process(); /* データ演算等の処理 */
break;
case
DISPLAY: do_display(); /* データや演算結果の表示 */
break;
case
ABORT: do_abort(); /*
終了前の処理 */
break;
default: printf("Input number %d-%d\n", LOAD, ABORT);
}
}
while (op != ABORT); /* ABORTが入力されるまでループを繰り返す */
return
0;
}
/*
以下、各関数を定義する */
void
do_load(void) {
printf("Data loaded.\n");
}
void
do_save(void) {
printf("Data saved.\n");
}
void
do_process(void) {
printf("Data processed.\n");
}
void
do_display(void) {
printf("Data displayed.\n");
}
void
do_abort(void) {
printf("Aborted.\n");
}
[解説]
エラーコードとエラー処理
[Advanced Topic] 関数へのポインタ
このプログラムは、「キーボードから入力されたコマンドに従って幾つかの関数に分岐させることを、終了のコマンドが入力されるまで繰り返す」というだけの動作をします。
この時点では、個々の関数は単に文字列を表示するだけものですが、これからそれぞれを実際に動作するものに作り上げていくわけです。もちろん、これらの関数の中でさらに分岐させて別の関数を呼び出すようにしたほうがよいケースもあるでしょう。
各関数の引数はvoidにしてありますが、実際には適切な引数を渡すようにする必要があります。どのような引数を渡せばよいか良く考えて、各関数を作成するようにして下さい。
各関数の返却値もvoidにしてありますが、これも状況に応じてエラーコードを返すようにするとよいでしょう。関数が返してきた値によってエラー処理を行うことができるようになります。(上の[解説]を参照)
なお、上の例のdo_abortに相当する関数は別に無くても構いませんが、動的に確保したメモリの解放など、プログラム終了前に行う処理のために使うようにしてもいいでしょう。
0.1.2 関数作成の順番
それぞれの関数は、どのような順番で作成しても構いません。ただ、データ配列の中身を確認できた方が何かと便利なので、データや結果の配列の中身をモニタに出力する関数(上の例ではdo_display)を始めに作っておくといいでしょう。
演算用の関数に取りかかるのは、ファイルからのデータの読み込みをできるようにしてからのほうが、動作チェックがやり易いと思います。ただし、ファイルの取り扱いに自信のないひとは、例えば以下のように適当な値で初期化した配列を使って、先に演算の関数を作っても構いません。
#define
SIZE 10
double
data1[SIZE] = {0.12, 1.23, 2.34, 3.45, 4.56, 5.67, 6.78, 7.89, 8.90, 9.01};
double
data2[SIZE] = {9.87, 8.76, 7.65, 6.54, 5.43, 4.32, 3.21, 2.10, 1.09, 0.98};
0.2 データの取り扱い
個々の関数を作っていくにあたり、取り扱うデータの構造についての基本方針を決めておかなくてはなりません。最低限の要求として、
元データの格納用に2つのdouble型1次元配列
演算結果の格納用に1つのdouble型1次元配列
を使うものとします。あとは自分自身のレベルにあわせて方針を決めて下さい。
方法(1):メモリ領域の動的確保に自信がないひとは、例えば、
#define
ARRAY_SIZE 10
...
double
data1[ARRAY_SIZE], data2[ARRAY_SIZE], result[ARRAY_SIZE];
などのように、データサイズを固定して、配列を静的に定義してしまうとよいでしょう。
方法(2):任意のサイズのデータに対応できるようにするには、例えば、
double
*data[2], *result;
と、ポインタを宣言しておき、適宜メモリを動的に確保して使うようにします。
方法(3):データのサイズなどをメンバに含めた構造体をつかうのも良い方法です。例えば、
typedef struct data_record {
long
length;
double
*buf;
}
myDATA;
...
myDATA *data[2], *result;
どのような構造体にするかは工夫次第です。例えば、元のファイル名とか、セーブしたかを示すフラグとかもメンバにするといいかも知れません。
方法(4):データと演算結果には構造上の違いはないので、単に3つの等価なデータバッファを使う、という方針でもよいでしょう。
myDATA *buf[3];
例えば「buf[0]とbuf[1]の演算結果をbuf[2]にいれる」とか、「ファイルの中身をbuf[1]に読み込む」とか、「buf[2]の中身をファイルに書き出す」とか、「buf[2]の中身をbuf[0]に入れる」といった操作を、各関数内で行えばよいわけです。
なお、いずれの場合にも、データ配列(あるいは構造体)は、グローバル変数ではなく、main関数内のローカル変数として宣言して下さい。
[解説]
ローカル変数とグローバル変数
0.3 アップグレード
プログラムが一通り完成し、その上でなお余力がある人は、さらに機能をアップグレードしてみましょう。
例えば、二次元配列データも扱えるプログラムにチャレンジしてみるのもよいでしょう。
また、扱えるデータの数を2つに限定せず、多数のデータを動的に扱えるプログラムにしてみるというのも、やりがいがあると思います。