] > development(2)

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

本日の内容


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

2-1. #include文

前回、関数は一つ一つ別々にコンパイルして個別にテストを行う方が望ましい と説明しました。 また、ある関数から別のファイルにある関数を呼び出す時、関数名と入出力の 型を明らかにするプロトタイプが必要であることを説明しました。 このプロトタイプは、別ファイルの関数の呼び出しやテストのプログラムな どで必要になります。 しかし、ファイルが増える度に、プロトタイプのような共通の情報を書くのは 無駄ですし、ミスを招きかねません。 従って、共通の情報は一箇所に集約し、互いに共有できる方が望ましいです。 C 言語では、共通ファイルを呼び出すための仕組みとして #include があります。 これはコンパイラがコンパイルの前処理(プリプロセス)する段階 で、ファイルを結合します。 構文は以下の通りです。


#include "ファイル名"

"(ダブルクォーテーションマーク) でファイル名を囲むと、コンパイル するディレクトリの中からファイルを探します。 一方、これとは別の構文があり、ファイル名を < と > で囲むと、コン パイラ専用のディレクトリから探します。 UNIX では /usr/include、配っている Windows 用の gcc-2.95.2 では c:\gcc-2.95.2\i386-mingw32msvc\include、 現在配布中の mingw-jp では c:\mingw-jp\include の中から探します。

C 言語を最初に習う時に述べた「#include <stdio.h> というおまじな い」は、実際には printf 関数のプロトタイプなどの書いてある stdio.h と いうファイルをコンパイラ専用のディレクトリから読み込むと言う意味だった わけです。 なお、 printf 関数などのプログラム作成に有用な関数はあらかじめ用意され ており、実行ファイルを作る時には自動的にリンクされるようになっています。

演習2-1

stdio.h の内容を読み、答えなさい。

  1. EOF とはなにか?
  2. getchar のプロトタイプを書きなさい。
  3. printf のプロトタイプを書きなさい。

さて、では、組合せの数を求める問題に戻ります。 前回 3 つの部分にわけました。 3 つのファイルでプロトタイプは共通でしたので、これを 別のファイルに保存し、 #include で呼び出すことにします。 C 言語ではこれをヘッダファイルと呼び、ファイル名のあとに .h をつけるのが慣例となってます。 ここでは combi.h という名前にしましょう。

-------------combi.h-------------
int factor(int n);
int combination(int n, int m);

これを各プログラムが読むように先週示したファイルを変更すると次のようにな ります。


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


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


/* testf.c */
#include <stdio.h>
#include "combi.h"
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");
    }
  }
}


/* testc.c */
#include <stdio.h>
#include "combi.h"
main(){
  int in1[]={0,1,5,-1};
  int in2[]={0,1,2,-1};
  int out[]={1,1,10,-1};
  int i,j;

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


/* testm.c */
#include "combi.h"
int combination(int n, int m){
  if((n==5)&&(m==2)){
    return(10);
  }else{
    return(0);
  }
}

ここでは全部のファイルで読み込むようにしました。 このようにすると、冗長なプロトタイプの読み込みは害にならない一方、 共通の情報をなにか付け足すことがあった時、 combi.h のみを書き換えれば 全てのプログラムに反映できるので便利です。

演習2-2

このようにできたプログラムをコンパイルして、リンクすることにより、 本来のプログラムと、テストを実行させなさい。


2-2. 条件付コンパイル

stdio.h の内容を見ると #ifdef や #ifndef といった行が存在します。 たとえば次の例を考えます。


#ifdef TEST
printf("Now, testing...\n");
#endif

ここでもし TEST が定義されていたらこの printf 文はそのまま使われますが、 TEST が定義されてなければ #endif までの行はなかったことになります。 定義は #define 文でもできますが、コンパイルの時に次のように指定するこ ともできます。


gcc -DTEST -c factor.c

つまり、同じソースコードでも -DTEST を付ける時と付けない時で違うプログ ラムをコンパイルすることができます。 これを 条件付コンパイル と言います。 例えば、次のようにすると、 -DTEST を指定してコンパイルした時だけ、呼び 出された値を画面に表示するようになります。


/* factor.c */
#include "combi.h"
int factor(int n){
  int i,k;
#ifdef TEST
  printf("factor(%d)\n",n);
#endif
  k=1;
  for(i=2;i<=n;i++){
    k*=i;
  }
  return(k);
}


/* combination.c */
#include "combi.h"
int combination(int n, int m){
  int a,b,c;
#ifdef TEST
  printf("combination(%d,%d)\n",n,m);
#endif
  a=factor(n);
  b=factor(m);
  c=factor(n-m);
  return(a/b/c);
}

なお、反対に #ifndef は定義されなかった時に有効になります。

演習2-3

上のプログラムを -DTEST 付きと、 -DTEST なしでコンパイルして、動作を比 較しなさい。

2-3. make

関数をファイルごとに分割すると、プログラムの開発においてテストが容易に なるなどのメリットはありますが、管理が大変になります。 特に、一つのファイルを変更した時、実行ファイルを作る作業は複雑です。 例えば、 factor.c を変更した時は factor.c だけコンパイルしてリンクすれ ば良いですが、 combi.h を変更した時は全てのファイルをリコンパイルする 必要があります。

MS-DOS では複数のコマンドをまとめて実行するバッチファイル というものがあります。 これは、コマンドラインで打つコマンドをそのままファイルに列挙し、ファイ ル名の拡張子を .bat にしたファイルです。 これを使うと複数のファイルのコンパイルを自動化できます。 たとえば次のようなファイルを作ります。

----------combi.bat----------
gcc -c factor.c
gcc -c combi.c
gcc -c main.c
gcc -o combi.exe factor.o combi.o main.o

但し、これだと factor.c だけを変更しても combi.c や main.c もコンパイ ルされ、非効率です。

このようなプログラムの管理を専門に行うためのツールとして makeが存在します(gcc-2.95 のパッケージや Visual C++ などに付録として付いてきます)。 Makefileというファイルを作ることで、最小限のコンパイルのみ を行い、自動的に目的の実行ファイルを作ることができます。 Makefile の書式は次の通りです。


ターゲット名1: 依存ファイル1 依存ファイル2 ...
(tab)実行すべきコマンド1
(tab)実行すべきコマンド2
...
ターゲット名2: 依存ファイル1 依存ファイル2 ...
(tab)実行すべきコマンド1
(tab)実行すべきコマンド2
...

ここで(tab)は tab コードです。空白と同様に画面には何も表示されませ んが、空白記号では正常に動作しませんので注意して下さい。

さて、ここで、今まで作成したソースファイルのコンパイル手順を忠実に書く と次のようになります。


combi.exe: factor.o combi.o main.o
       gcc -o combi.exe factor.o combi.o main.o
testf.exe: factor.o testf.o
       gcc -o testf.exe factor.o testf.o
testc.exe: factor.o combi.o testc.o
       gcc -o testc.exe factor.o combi.o testc.o
testm.exe: testm.o main.o
       gcc -o testm.exe testm.o main.o
factor.o: factor.c combi.h
       gcc -c factor.c
combi.o: combi.c combi.h
       gcc -c combi.c
main.o: main.c combi.h
       gcc -c main.c
testf.o: testf.c combi.h
       gcc -c testf.c
testc.o: testc.c combi.h
       gcc -c testc.c
testm.o: testm.c combi.h
       gcc -c testm.c

但し、 make には様々な情報があらかじめ含まれているため、もっと省略した 書き方が可能です。 まず、Makefile では変数が使えます。 例えば、C コンパイラの名前は CC という変数にすでに格納されています。 但し、それは cc という名前となってますので、 gcc を使う時にはあらかじめ cc の代わりに gcc を設定する必要があります。 具体的にはCC=gccと書きます。 なお、変数を参照する時は $(CC) のように書く必要があります。

また、$@ はターゲット名を示す変数、$^ は全ての依存ファイルの名前の列を 表す変数です。

さらに、make は「xxx.c というファイルから xxx.o というファイルを作るに は C コンパイラを利用する」と言うことをあらかじめ分かってます。ですから、 xxx.c から単純に xxx.o が作られるだけならそのルールは書かなくて済みま す。つまり、次のようなルールはまるごと不要です。


a.o: a.c
(tab) $(CC) -c a.c

また、依存関係は複数行書けます。つまり factor.o: factor.c combi.h は factor.o: factor.c と factor.o: combi.h という行に分割できますが、ここ で make はすでに factor.o: factor.c が分かっていますので、これも省略で きます。従って、factor.o: combi.h だけ書けばいいことになります。

つまり次のように簡略できます。


CC=gcc
combi.exe: factor.o combi.o main.o
       $(CC) -o $@ $^
testf.exe: factor.o testf.o
       $(CC) -o $@ $^
testc.exe: factor.o combi.o testc.o
       $(CC) -o $@ $^
testm.exe: testm.o main.o
       $(CC) -o $@ $^
factor.o: combi.h
combi.o: combi.h
main.o: combi.h
testf.o: combi.h
testc.o: combi.h
testm.o: combi.h

これを Makefile という名前で保存し、 make combi.exe と打つ と combi.exe ファイルが自動的に作られます。 なお、 make だけ打つと自動的に先頭のターゲットが作られます。 また、 make -n と打つと、実行されるコマンドが表示されます。

なお、CFLAGS という変数がコンパイル時の時に渡されます(初期値は空)。 従って、条件コンパイルをするには Makefile の中で次のように変数の値を設 定します。


CFLAGS=-DTEST

演習2-4

  1. make により、CFLAGS を指定せずに実行ファイルを作りなさい。
  2. Makefile に次の文を付け加えなさい。
    
    clean:
    (tab) del *.o
    
  3. make clean を行ったあと、 CFLAGS を指定した実行ファイルを作りなさ い。作られた実行ファイルがテスト用のものであることを確認しなさい。

2-4. Java のプログラム管理

Java 言語は C++ 言語の欠点を補正したような言語ですので、基本的には C++ にも C にも良く似ています。 しかし、C++ とは違い、Java では C 言語そのままのプログラムは動作しませ ん。

Java では全ての関数や、変数はクラスに所属している必要があります。 変数や関数に static を指定するとオブジェクトの生成とは関係なくなります。 変数はクラス変数と呼ばれ、唯一性が保たれます。 一方、関数はクラス関数と呼ばれ クラス名.関数名(引数) で呼び出すことができます。

Java にはパッケージという概念があり、Java のクラスはパッケージ毎に管理 されています。 何も指定しない時は空のパッケージが指定されていると仮定されます。 基本的には変数やメッソドは同じパッケージ間で共有できます。 一方、public 指定をすると、他のパッケージからアクセスできます。 また、 private を指定すると同じクラス内でないとアクセスできなくなりま す。 なお、パッケージは具体的にはディレクトリによって管理されています。 同じパッケージに含まれるクラスは同じディレクトリに存在しなければなりま せん。 なお、本当のクラス関数の呼び出しはパッケージ名.クラス名.関数名 (引数)ですが、パッケージ名が空であれば、上で説明した通りパッケー ジ名がない形で呼び出せます。

Java コンパイラ(javac)はソースファイル(.java)の中のクラス名と同じファ イルを作ります。 実行時にはjava クラス名とすると、その指定したクラスの中の static main 関数を呼び出します。 なお、static main 関数は引数として String[] をとり、戻り値はありません (void 型)。 C 言語と違い戻り値の型は必ず指定しなければなりません。 また main 関数は外部から呼び出す必要があるので public を指定します。

例えば、次のプログラムを考えましょう。


----------------e.java------------
class a {
    static void b() {
	System.out.println("abc");
    }
}
class c {
    public static void main(String[] args){
	a.b();
    }
}
----------------f.java------------
class d {
    public static void main(String[] args){
	a.b();
    }
}

どちらもパッケージ名を指定していないので、同じ(空の)パッケージです。従っ て、同じディレクトリにあれば、 class d からでも class a の関数を呼び出 すことができます。 この、 e.java をコンパイルすると、 a.class, c.class ができます。 また、 f.java をコンパイルすると d.class ができます。 そして、java c で、クラス c 内の main 関数を呼び出しても、 java d でクラス d 内の main 関数を呼び出しても、class a の関 数を呼び出せます。 同じクラス内に同じ名前の関数(main)は含めませんので、class a の関数のテ ストを行うにはこのように同じパッケージ内に別のクラスを作ってテストを行 う必要があります。

但し、同じパッケージのクラスは同じディレクトリになければならないので、 主プログラムをテストするために仮のクラスを生成する時は、テスト用のクラ スを上書きしなければなりません。 そのため次のようなファイル構成にする必要があります。


----------------real.java------------
class a {
    static void b() {
	System.out.println("This is the real function.");
    }
}
----------------main.java-------------------
class main {
    public static void main(String[] args){
	a.b();
    }
}
----------------test.java------------
class a {
    static void b(){
	System.out.println("This is for test.");
    }
}

real.java をコンパイルすると a.class が作られ、 main.java をコンパイル すると main.class が作られます。ここで java main とすると正 しく real.java 内の a.b() が呼ばれます。 ここで、test.java をコンパイルすることにより a.class を上書きすると、 java main でテスト用の a.b() が呼ばれることになり、 main() のテストを行うことができます。

2-5. ビルダの利用

Borland C++ Builder、Visual C++、Code Warrior、gcc developer station 2000 など、統合環境といわれるソフトウェアは(裏で make と同じ処理をして)、 プログラムのソースファイルを管理します。 これらのソフトでは、プログラムの管理の単位を「プロジェクト」と呼んでい ます。 プログラムを作る際には「プロジェクト」にソースコードを登録します。 この時、 #include で呼ばれるヘッダファイルは自動的に解釈されます。 そして、プログラムが終了したら、ビルドを指示すると、コンパイル、リンク を自動的に行います。

なお、これらの統合環境では Windows 用のアプリケーションの作成支援も行っ ているので、文字だけの画面を前提としたプログラム(コンソールアプリ)と Windows の GUI 環境用のプログラムでプロジェクトの設定が異なります。 この区別は、プロジェクトを新規作成する時に選択します。 選択を間違えるとプログラムが正しく実行できなくなりますので注意して下さ い。

前回の、 factor.c combination.c main.c testf.c testc.c testm.c に対し ては、次の 4 つのプロジェクトを作ると、テストなども全て管理できます。

  1. factor.c combination.c main.c
  2. factor.c testf.c
  3. factor.c combination.c testc.c
  4. testm.c main.c

なお、 Borland C++ Builder でコンソールアプリケーションを実行させると、 プログラムが終了した瞬間に画面が消えてしまいます。 その結果、そのままではプログラムの実行結果を見ることができません。 これを防ぐためプログラムの最後に getchar(); など、意味のない入力待ちを させる必要があります。

2-6. プログラムの管理

大きなプログラムを作っていると、変更箇所を元に戻したくなったり、微妙に 変更を加えた複数のプログラムを作りたくなったりします。 また、複数の人間でプログラムを作成する場合、同時に別々に修正を加えると 一方の修正が無駄になったりします。 このような不都合を取り除くためには、ソフトウェアのファイルを管理する必 要があります。 RCS(Revision Control System) や CVS(Concurrent Versions System) はこの ような問題を解決するためのものです。

RCS

RCS ではファイルを次のように管理します。 check in をするとファイルは登録され、ファイルの変更はできなくなること で保護されます。また、「ファイル名,v」というファイルが作 られます。 check out すると、ファイルが読み書き可能になります。また、複数回の check out はできなくなっているため、「他人が chech out 中のファイルを さらに check out できない」ということで修正することを禁止します。 check out したファイルを再び check in するとコメントが求められます。 なお、ファイルの中に $Id$ という文字列を入れておくと、 check in の時 にファイル名、バージョン、 check in した日付(世界標準時)、 check in し た人の名前、状態などが $Id: combi.c,v 1.2 2003/09/26 11:55:26 sakamoto Exp$ というような書式で挿入されます。 コメントの中に入れておくなどするとバージョン管理がさらにし易くなります。

Meadow ではこの操作を容易にしています。 最初の登録は C-x v i でできます(C- は Control キーを押しながらの意)。 そして、以後 C-x C-q で check out, check in を繰り返します。 Check in ではコメントを求められます。コメントを入力したら C-c C-c で入 力終了です。

check in の際にバージョンの値を指定したり、check out の際に、どのバー ジョンのファイルを出すかなどを指定できます。 これは Meadow からはいずれも C-u C-x C-q でできます。

CVS

RCS では複数人が同時に修正することを禁止していました。 これは能率面ではマイナスです。 ですから、他人が check out しているために別の人が修正が妨げられること がなるべく少なくなるようにすべきです。しかし、これは全ての人がこのシス テムとは別に check in, check out のタイミングを管理しなければならない ことになります。 一方、 CVS は始めから複数人が同時にファイルの修正が可能なように作られ ました。 ですから、このシステムでは修正を妨げられることはありません。 CVS はサーバ/クライアント方式をとっています。 サーバはリポジトリという領域を管理し、クライアントはそこからファイルを check out します。 そして、クライアントはサーバーにファイルの修正を登録(commit)すると、他 の人からの修正と矛盾しないかチェックします。 矛盾している場合は、 commit する人に矛盾点を提示し解消させます。 これは、見ず知らずの相手と協調してプログラム開発ができるため、 現在のインターネットでのフリーソフトの開発に広く使われています。 なお、本講義ではサーバの必要な CVS 環境は提供しません。 興味がある人は、 バージョン管理システム CVS を 使う(http://radiofly.to/nishi/cvs/) windowsユーザのためのcvs入門(http://www007.upp.so-net.ne.jp/kengai/linux/cvs.html) などを参考にして下さい。


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