] > development(1)

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

本日の内容


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

1-1. 講義の前に

この講義の狙い

この講義ではアルゴリズムとデータ構造を学びます。 主に、触れる理論はグラフ理論とオートマトン理論です。 使用するプログラミング言語は C 言語ですが、 C++ と Java にも触れます。

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

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

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

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

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

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

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

そこで、良く使われている手法は「入力(必要な情報)と出力(得られる情報)が 単純ではっきりしている部分」を取り出すことです。このような部分が取り出 せれば、単純にその入力と出力の条件を満たすように自由にプログラムを作る ことができますし、その部分だけをテストすることも容易です。 入力が決まれば出力が決まるようなものを、数学では関数と言い ます。 C 言語でも関数と言い、C++ 言語でも C 言語と同様に関数が使用できます。 一方、入力が値だけではなくメッセージも受け付け、情報を保持できる機能を 持つオブジェクトという概念を C++ や Java で使用できますが、 このオブジェクトを使用もプログラムの分割を容易にします。 但し、オブジェクトの詳しい話題は他の講義に譲ります。 次の節では関数について説明します。

関数

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

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

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

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

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

ここでは階乗を表す関数を factor(n) と表すことにしましょう。 この関数は整数を一つ与えられると、整数を一つ返します。 C 言語で関数を使用するには、このように入力する値をカッコの中に入れます。 そして、得られた値を sin(x) などの数学の関数と同じように数式で使います。 但し、C 言語を含む多くのプログラミング言語では、関数の出力はひとつだけ という制約があります。

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


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

これは、今まで書いてきた C 言語のプログラムにおいて main を書くのと似 ています。 但し、最初の 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 をそれぞれ別々 に作ることができますし、場合によっては複数の人間で作ることもできます。 また、それぞれの関数をテストすることも可能になります。 さて、プログラムのテストを考えます。 関数が一つ一つ正常な状態にするため、一つ一つの関数を別々にテストするこ とを考えます。 すると、上の呼び出す、呼び出されるという関係から、次のことが導かれます。

  1. factor(n) が正常でない限り、 combination(n,m) は正常でない。
  2. combination(n,m) が正常でない限り main() は正常ではない。

つまり、呼び出す、呼び出されるという関係から、次のようなテストをする関 数の順番が得られることになります。

  1. factor(n)
  2. combination(n,m)
  3. main()

さて、では次に factor(n) をテストするにはどうすれば良いでしょうか?

分割コンパイル

C 言語では 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,j;

  for(i=0;in[i]!=-1;i++){
    j=factor(in[i]);
    printf("factor(%d)=%d: ",in[i],j);
    if(out[i]==j){
      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() しか呼び出しませんので、 次のように作ることで combination(5,2) だけはちゃんとした値を返すプログ ラムができます。


/* 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>
東京電機大学工学部情報通信工学科