] > development(1)

第 1 回 プログラム開発(1)

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

1-1. 講義の前に

この講義の狙い

この講義ではアルゴリズムとデータ構造を学びます。 主に、触れる理論はグラフ理論とオートマトン理論です。 使用するプログラミング言語は C 言語ですが、 C++ 、 Java、 Smalltalk にも触れます。 目標は複雑で大きなデータを処理するための手法を学ぶことです。

期末試験は行わず、評価はレポート二通で行います。

プログラミング言語の違い

プログラミングの業界では「車輪を発明するな」という言葉があります。 これは、既知のさまざまな基本的なテクニックは自分で開発せずに、先人の残 したプログラムを利用すべきだということです。 しかし、この授業ではその車輪(つまり基本的なテクニック)について学びます。 したがって、 C 言語を使って車輪(基本的なテクニック)を発明するようなこ とをします。

一方、 C++ 言語には、基本的なテクニックがすぐに使えるように、それらを集めた STL(Standard Template Library)が使えます。 また、 Java には java.util というクラスライブラリがあります。 同様に Smalltalk には Collection カテゴリがあります。 そこで、そのようなライブラリのない C 言語で原理やテクニックを紹介しま すが、一方で、 C++ や Java や Smalltalk にそれに対応するライブラリがあった場 合、そのライブラリの使用法を説明します。

1-2. 関数のテスト(1)

この講義ではやや複雑なプログラムを作成します。 そのために、プログラムの開発上の手法を学びます。

プログラムを作成する上で重要なことは、いかに正常に動作することを保証す るかです。 そのために利用するのは、多くのプログラミング言語で実現されている プログラムの分割です。 しかし、どのように分割するかで、プログラミングの効率が変わります。 分割する上でもっとも重要なことは、分割する断面が単純であることです。 そうでないと分割されたプログラムを組み合わせる際に困難が生じます。 例えば、単純にプログラムを前半と後半などに分割すると、前半と後半の関係 を全て考えなければなりませんし、後半部分だけをテストのは非常に難しいで す。

そこで、良く使われている手法は「入力(必要な情報)と出力(得られる情報)が 単純ではっきりしている部分」を取り出すことです。このような部分が取り出 せれば、単純にその入力と出力の条件を満たすように自由にプログラムを作る ことができますし、その部分だけをテストすることも容易です。 入力が決まれば出力が決まるようなものを、数学では関数と言い ます。 C 言語や C++ 言語でも関数と言います。

関数

例えば、 n 個のものから m 個のものを取り出す組合 せの数を考えます。この組合せの数は次の式で表せます。

nCm = n! m! (n-m)!

これを素朴に定義通り計算することを考えます。

C 言語では関数という仕組みでプログラムを分割できます。 そこで、この計算をするのに階乗を計算してから最終的な値を求めることを考 えます。 つまり、階乗を計算する関数を作り、そして、その関数を元に組合せの数を求 めます。 さらに組合せの数を求める部分も関数として考えることができます。 つまり、 ある値の組合せの数を求めるプログラムは次の 3 つの部分に分割できます。

  1. 階乗を計算する関数
  2. 階乗を計算する関数を呼び出し、組合せの数を求める関数
  3. 求めたい値を定め、組合せの数を求める関数を呼び出し、結果を出力する 部分

ここでは階乗を表す関数を factor(n) と表すことにしましょう。 この関数は整数 n 与えられると、 n! の値となる整数を一つ返します。 C 言語で関数を使用するには、このように入力する値を丸カッコの中に入れます。 そして、得られた値は sin(x) などの数学の関数と同じように数式で使うこと ができます。

factor(n) の定義は、次のように行います。


int factor(int n){
 n から factor の値を計算する仕方
 return(求めた値);
}

最初の int は出力される値の型を表し、カッコの中の int は入力され る値の型を表しています。 factor の計算法の中では入力された値は n を使っ て計算します。

さらに、組合せの数の計算も関数で書くと次のようになります。


int combination(int n, int m){
  n と m と factor 関数を使った組合せの数の計算の仕方
  return(求めた値):
}

プログラムをまとめて書くと次のようになります。


#include <stdio.h>

int factor(int n){
 factor の計算法
 return(求めた値);
}
int combination(int n,m){
  factor 関数を使った組合せの数の計算
  return(求めた値):
}
main(){
  int n,m;
  printf("Input n and m: ");
  scanf("%d%d",&n,&m);
  printf("%dC%d =  %d\n",n,m,combination(n,m));
}

このようにプログラムを 3 分割できました。 main() は combination(n,m) を呼び出し、 combination(n,m) は factor(n) を呼び出します。 このようにすると、factor や combination や main をそれぞれ別々 に作ることができますし、場合によっては複数の人間で作ることもできます。 また、それぞれの関数をテストすることも可能になります。

さて、ここでプログラムのテストを考えます。 一つ一つの関数が正常な状態になるように、それぞれ別々にテストすることを 考えます。 では、まず factor(n) をテストするにはどうすれば良いでしょうか?

分割コンパイル

C 言語で作ったプログラムを実行すると main 関数が呼び出されます。 他の関数は main 関数から呼び出すか、 main 関数から呼び出された関数から呼び出すなどしなければなりません。 従って、本来の完成したプログラムとは別にテストを行いたい場合は、本来の main 関数と別のテスト用の main 関数が必要になります。 しかし、異なる main 関数は同時に二つ以上存在できません。

C 言語では分割コンパイルという手法により二つの main 関数を 利用することができます。 分割コンパイルとは、プログラムのファイルを分割し、それぞれを別々にコン パイルし、最後に結合する手法です。 例えば、 factor.c という factor() 関数だけを含んだファイルを factor.o という中間ファイルにコンパイルできます(これをオブジェクトファイル と呼びます)。別に combination.o と main.o というオブジェクトファイルを 作り、最後に factor.o, combination.o main.o を結合して一つの実行 ファイルを作ることができます。このオブジェクトファイルを結合する ことをリンクと呼びます。

この手法を使うと、これとは別に、 factor() をテストする main 関数を含む ファイル testf.c から testf.o を作り factor.o と testf.o をリンクすれば factor() をテストする実行ファイルを作ることができます。

gcc を使って factor.c から factor.o を作るには次のようにします。


gcc -c factor.c

一方 factor.o と combination.o と main.o から実行ファイル combination.exe を作るには次のようにします。


gcc -o combination.exe factor.o combination.o main.o

プロトタイプ

ところが、combination.c は単純にはコンパイルできません。combination() は factor() を呼び出しますが、factor() の入出力の情報がないとコンパイ ラは呼び出しを処理できません。 そのため、入出力の情報を与える文を書く必要があります。関数の入出力の情 報だけの記述をプロトタイプと呼びます。 factor() のプロトタイプは次のようになります。


int factor(int n);

このようにプロトタイプは関数定義において実際の手続きの代わりに ;(セミ コロン)のみで終らせた形になります。 combination.c の最初にこれを書いておけば factor() の計算法などは書かな くても combination.c をコンパイルすることができます。

演習1-1

次の 3 つのファイルをそれぞれコンパイルし、一つの実行ファイルにリンク しなさい。



/* factor.c */
int factor(int n){
  int i,k;
  k=1;
  for(i=2;i<=n;i++){
    k*=i;
  }
  return(k);
}


/* combination.c */
int factor(int n);
int combination(int n, int m){
  int a,b,c;
  a=factor(n);
  b=factor(m);
  c=factor(n-m);
  return(a/b/c);
}


/* main.c */
#include <stdio.h>
int combination(int n, int m);
main(){
  int n,m;
  printf("Input n and m: ");
  scanf("%d%d",&n,&m);
  printf("%dC%d =  %d\n",n,m,combination(n,m));
}

ヒント

以下の手順を行います。

  1. gcc -c factor.c
  2. gcc -c combination.c
  3. gcc -c main.c
  4. gcc -o a.exe factor.o combination.o main.o

現代風プログラミングスタイルとテストファースト

パーソナルコンピュータが普及するまではコンピュータが高額だったため、プ ログラム開発になるべくコンピュータを使用しない工夫がされてました。 まず、ウォーターフォールモデルという手順が用いられていました。 これは設計、コーディング、テストの工程を分割します。 そして、水が高いところが低いと ころへ流れるように設計を終えてから、プログラムを書き、プログラムを書い てからテストを行うという手法です。 このようにするとコンピュータを実際に必要とするのがテストの期間だけにな るので、コンピュータの使用料を減らすことが出来ます。 しかし、このような設計法では各工程が完璧に終らなければならないという、 まず不可能な仮定を前提としています。 実際は、テスト段階で重大な誤りが見つかることがあります。 そして、さらにその時にはプログラム作成が終了していますので、修正のため にほとんどが書き直しになったりします。

また、誤りが発見された時、それを人間がチェックしやすくなるよう にさまざまな書類が作られていました。 その中には、フローチャート、変数表、関数(サブルーチン)名表などがありま した。 問題が発生した時にはこれらを活用してコンピュータを使用せずに間違いを発 見していました。

しかし、パーソナルコンピュータなどでプログラムを開発する場合、このよう な手法や書類によりコンピュータの利用を節約する必要はありません。 したがって、プログラムのチェックやテストをコンピュータに自由に行わせた 方が良いです。

XP(エクストリームプログラミング)は、多くのプロ グラミングの効率的な手法を組み合わせたものです。 設計もプログラム作成もテストも同時進行させ、小さいプログラムを徐々に大 きくしていくような開発モデルです。 この中に「テストファースト」という手法があります。 これは、出来上がるプログラムより先に、それを自動的にテストするプログラ ムを先に作るというプログラミングスタイルです。 テストファーストにより、次のような利点が生じます。

テストは、エラーが生じそうな部分に対して書き、正常に動作することが明ら かな部分については省略します。

factor() のテストの例を次に示します。



/* testf.c */
#include <stdio.h>
int factor(int n);
main(){
  int in[]={0,1,5,-1};
  int out[]={1,1,120,-1};
  int i,result;

  for(i=0;in[i]!=-1;i++){
    result=factor(in[i]);
    printf("factor(%d)=%d: ",in[i],result);
    if(out[i]==result){
      printf("Ok\n");
    }else{
      printf("NG\n");
    }
  }
}

演習1-2

この testf.c をコンパイルし、 factor.o とリンクし、テストを実行しなさ い。

演習1-3

combination()をテストするプログラムを書き、実際にテストしなさい。 以後ここでできたプログラムを testc.c と呼ぶことにします。

スタブ

main 関数をテストするプログラムはどのように書けば良いでしょうか? main 関数は、出来上がったプログラムを実行すると最初に呼び出される関数 ですから、main()を呼び出す関数を作ってもそれを実行することはできません。 そこで、main() をテストすには別の方法を考えます。 それは、main() 自身が呼び出す関数の単純なダミーを作っておき、 main() とリンクして、 特定の動作を main() にさせると言うものです。 組み合わせの数を求めるプログラムでは main() は combination() しか呼び 出しませんので、 次のように 5 と 2 を入れた時にだけ正常に動作するニセの combination 関 数を作ることで、 combination の計算に関わるところ以外の main 関数のチェッ クをすることができます。 このように呼び出し側をだますためのテスト用のダミーのプログラムを スタブと呼びます。


/* combination.c */
int factor(int n);
int combination(int n, int m){
  if((n==5)&&(m==2)){
    return(10);
  }else{
    return(0);
  }
}

このプログラムを main() にリンクすることで、 main() が combination(5,2)を呼び出す時だけ main() をテストすることができます。

参考: オブジェクト指向

オブジェクト指向とは、プログラムの中で一つの独立した機能を、内部的に一 つのアプリケーションソフトのように扱うようにして、プログラムを分割する 技術です。 その独立した機能を持つものを オブジェクトと呼びます。 オブジェクトを操作するには、メッセージを利用します。 メッセージの多くは「動詞 + 目的語」の形をしていますが、この動詞のこと をメソッドと呼びます。 そして、オブジェクトは内部に記憶領域を持ち、メッセージを解釈しながら計 算を行います。

オブジェクト指向プログラミングでは作成すべきプログラムをオブジェクトに 分割します。 そして、それぞれのオブジェクトの必要な機能をメソッドとして登録し、メソッ ドの操作をプログラムとして書きます。 そして、それぞれのオブジェクトのテストを行った後、主たるプログラムでオ ブジェクトを操作するプログラムを書きます。

なお、本講義はオブジェクト指向を教えるのが目的ではないので、詳しい説明 は他の講義に譲ります。


坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科