CONTENTS / BACK-PAGE / NEXT-PAGE

今回は演習です。

 今回は今までの復習を兼ねて、いろんなプログラムを組んでみましょう。問題提起からデータ構造、アルゴリズムの作成を経て、実際のプログラムを組んでみます。つまり演習です。(タイトルの意味がお分かりになったでしょ) 4章までの知識で、少し工夫すればたいがいの問題を解くプログラムは組むことができます。

 一口に”プログラムを組む”と言っても一つの問題に対していろいろな考え方のプログラムが存在します。今回は”良いプログラム”といわれるものを目標として作成することにします。今回の演習が、実際にプログラムを組むときの手助けになればと思います。

 できれば今回は、この原稿を先にプリントアウトして実際にプログラムを組み、メモを取りながら、読み進むと一層よく分かると思います。

 では、さっそく本題に入りましょう。今回は以下のような形式で行きたいと思います。

	0)問題を与える。
	1)問題を分析する。
	2)必要なデータとそのデータ構造を考える。
	3)全体の大まかな流れを考える。
	4)データ構造にあったアルゴリズムを考える。
	5)各所を微調整しながら、プログラムを書く。
	6)コンパイルしてエラーがないかどうか調べる。
	7)入力に対して、欲しい出力が得られるかどうかテストする。
	8)改良。
	9)完成。
 この流れは今回に限った形式ではなく、プログラミングの基本的な流れです。

 この流れをCでも他の言語でもプログラムを組める人から見ると”私達のやり方とはちょっと違う”という人がいるかも知れません。しかし、このアプローチ(問題解法に向けての方針)がこれから世の中で必要とされている方法論の流れなので、我々はこの流れに沿ってプログラミングすることにします。(別にこれ以外のやり方を批判するわけではありませんが)


では、最初の問題です。

問題1 : 文字列を受取り、その逆順を返すプログラムを書け。────────0)

 ”あれ、この問題、前にやったじゃない”と思ったあなた、あなたは偉い!!

 そう、確かにこの問題は第4章のプログラム4−13と似ています。

 しかし、よく見てください。前回のプログラムでは配列が”数値の配列”になっていましたが、今回は”文字列(文字の配列)”になっています。また、文字列の長さが指定してありません。これらのことは実際プログラムを組む時に大きな問題となるので、この段階でしっかり見極めておく必要があります。

 このように与えられた問題から、その必要とされているものを抽出し、必要な情報を引き出すことを、”問題を分析する”と言います。

 実際には、分析とは問題をもっと明確化して必要な情報を得て、プログラミングを容易にすることを言います。問題からだけでは必要な情報が得られないときは、出題者に対してその情報を聞くようにします。

 では、今回の問題を分析して、疑問点を挙げてみましょう。


1)問題1の分析─────────────────────────────1)

 (1)入力元は何で、出力先は何か。

 (2)文字列の長さはどれくらいか。

 この先でもっと疑問点が出てくるかも知れませんが、とりあえず、これくらいでしょうか。

 疑問点(1)は入力と出力が、標準入力(キーボード)、標準出力(モニタ)、ファイル、プリンタ、など多くあるデバイス(外部入出力装置)のどれなのかということです。プログラムによってはこれが非常に重要なことがありますが、Cでは通常、特に指定がない限り標準入力と標準出力を用います。

 疑問点(2)ですが、例えば先のプログラム4−13のintをcharにして、その意味を考えてみるとわかりやすいでしょう。


プログラム4−13改

/* program 4-13k         */
/* 再帰による出力例      */

#include <stdio.h>

void printrec( int n, char data[] );     /* int data[]を char data[]にする */

main()
{
    char data[] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'};
                                 /* データの内容も文字にするため''でくくる */
    printrec( 9, data );
    printf("\n");
}

void printrec( int n, char data[] )       /* int data[]をchar data[]にする */
{
    if ( n >= 0 )
    {
        printf("%5c", data[n]);          /* 出力フォーマットも%dを%cに直す */
        printrec( n-1, data );
    }
}

 しかし、このプログラムではプログラミングの前にあらかじめ文字列とその長さが決まっていることになります。

 今回の問題では”文字列を受けとり”とあるので、文字列を入力するようにしなければなりません。また、そうするとなると文字列の長さもあらかじめ分かりません。

 このような問題(配列の大きさがあらかじめ分からない)に対しては以下の二つの方法があります。

    ・配列を大きくとる。(例えば、char data[65535]など)
    ・再帰を用いて、無限に入力を受取り、無限に出力する。

 通常では前者の方法がとられます。何故なら通常のプログラムは入力されたデータをとっておき、それを基に計算したり変換したりして必要な出力を求めるからです。

 しかし、前者の方法では、あらかじめ決められた文字数以上の入力に対しての動作の保証がありません。例えば、例の char data[65535] では65536文字以上の文字列は扱えません。また、コンパイラによってはもっと小さな配列しかとれないものもあります。

 これに対して後者は、”データをとっておいてそれを加工して出力を得る”ことは出来ませんが、このような特殊な場合に威力を発揮します。(もっとも無限に受け取るといっても各マシンのメモリー数を越えることはできませんが。)

 とりあえず、今は前者で話しを進めることにしましょう。その方が話しがわかりやすいし、プログラムも組み易いからです。あと、プログラム4−13改のことは忘れて話をしましょう。あのプログラムは(4−13も)再帰プログラムの強引な例題なので、ここでは取り上げません。但し、ちゃんとした再帰プログラム(後者の方法)は後で組んでみましょう。

 次は必要なデータとそのデータ構造の選出です。この問題は比較的簡単だと思います。ちょっと考えてみてください。









 どうですか?? では、一緒に見てみることにしましょう。

 思い付くままに挙げてみることにします。


2)データとデータ構造の決定───────────────────────2)

・文字列を収納するデータ。データ構造は文字配列。

まだ他に必要となるものがでてくるかもしれませんが、とりあえずこんな所でしょうか。では、これに合わせて大まかな流れを考えてみます。


3)大まかな流れ─────────────────────────────3)

  (1)文字列を入力する。
  (2)逆順に出力する。

 これはプログラム4−13改からも明らかです。
 次に細かいアルゴリズムを考えてみましょう。


4)細かいアルゴリズムの決定───────────────────────4)

大まかな流れに肉付けする形で進めてみましょう。用意したデータは文字配列なので

  (1)配列に文字列を入力する。
  (2)配列の最後の要素から表示し、最初の要素まで逆順に表示していく。

もう少しくわしく考えてみましょう。
まず入力ですが、これには

  ・配列に一気に入力する。
  ・一文字づつ入力し、順次、配列に格納する。

の二通りが考えられます。具体的には(Cでは)

  ・scanf()を用いる
  ・getchar()を用いる

ということにそれぞれ当てはまります。

 多くの初心者は(特にベーシックからのユーザは)scanf()に走りがちですが、我々はあまりお勧めしません。というのはscanf()はとても扱いが複雑な部分を持つからです。といってもこれは初心者向けの講習ですから、とりあえずここではscanf()を用いることにします。scanf()を用いるとどうしてよくないのか、getchar()でどうやって文字列を入力するのかということについては後述します。

 次に進む前に少々前準備をしましょう。配列の最初の要素というのは簡単です。char data[65535]というデータの最初の要素は、data[0]ですね。(以降、説明では特に断わりがない限りこのchar data[65535]を用いることにします) では入力した文字列の最後の要素というのはどうでしょうか。data[65534]でしょうか。違いますね。入力した文字列の大きさが分からなければ、最後の要素は分かりません。例えば

abcde
という入力に対しては、

edcba
という出力がなされるはずです。これは

data[4], data[3], data[2], data[1], data[0]
という順番で出力させなければなりませんが、これには文字列の大きさ(5)が分かっていなければなりません。

 というわけでもう一つ、文字列の大きさというデータを加えましょう。また、ループを利用して逆順に出力するのですから、ループ制御用の変数が必要になります。さらに文字列の大きさを得るという作業を先の(1)と(2)の間に入れます。具体的には、strlen()という関数を用いることにします。

配列の大きさ = strlen(配列)
 さぁ、これで準備は整いました。具体的にアルゴリズムを書いてみましょう。

 ちょっと考えてみてください。









 まず、入力部です。

scanf("%s", 文字配列)
%sは文字列を指定するフォーマットです。つまりこれでscanf()で文字配列data[]に文字列を入力することをあらわします。

 次に入力した文字列の大きさ得るのですが、これはProgaramming Lecture 7、プログラム4−8−2で習った、標準ライブラリの<string.h>のstrcmp()の類のstrlen()を用いるということでした。

文字列の大きさ = strlen(文字配列)
 最後は出力部です。ここでは、すでに文字配列に文字列が入っているものとして話をします。とりあえずループはwhile文を使ってみます。

	制御用変数 = 文字列の大きさ - 1
	while(制御用変数 >= 0)
	{
	  文字配列[制御用変数]を出力
	  制御用変数 = 制御用変数 - 1
	}
 こんなところでしょうか。while文ではなくfor文を用いると以下のようになります。

 for(制御用変数=文字列の大きさ-1;制御用変数>=0;制御用変数=制御用変数-1)
   文字配列[制御用変数]を出力

 最後にdo文を用いた方法です。

	制御用変数 =  文字列の大きさ  - 1
	do{
	  文字配列[制御用変数]を出力
	  制御用変数 = 制御用変数 - 1
	}while(制御用変数>=0)
この問題では、どの制御文を用いてもシンプルなプログラムが書けるので、その選択は個人の自由でしょう。

 ところでなぜ、制御用変数は文字列の大きさから1引いたものを代入するのでしょうか。考えてみてください。(ヒントは既に述べてあります)

 ここまで書けば後はプログラムを書くのは簡単ですね。先のデータ宣言、入力部、出力部を組み合せればいいのです。では実際にプログラムを書いてみてください。上記のアルゴリズムに肉付けはもちろんですが、基本的なこと(#include, main(), {}の対応,セミコロン等)を忘れないように。自信のある人はコンパイルまでしてみてください。










5)プログラムを書く───────────────────────────5)

 実際の変数名を皆さんがどの様につけたかは分かりませんが、ここでは以下のようにします。

	文字配列           char data[65535]
	入力文字列の大きさ int leng
	ループ制御用変数    int i

では、プログラムの解答です。まずwhile文を用いたものです。

演習プログラム1−1

/*  Program Exercise1-1  */
/*  Reverse String  */

#include <stdio.h>
#include <string.h>

main()
{
    char data[65535];               /*  データ宣言部  */
    int i, leng;

    scanf("%s", data);              /*  入力部  */

    leng = strlen(data);            /*  文字列の大きさを得る  */

    i = leng - 1;                   /*  逆順に出力  */
    while(i >= 0)
    {
        printf("%c", data[i]);
        i = i - 1;
    }
}

次はfor文を用いたものです。前半部分は変りませんね。

演習プログラム1−2

/*  Program Exercise1-2  */
/*  Reverse String  */

#include <stdio.h>
#include <string.h>

main()
{
    char data[65535];                          /*  データ宣言部  */
    int i, leng;

    scanf("%s", data);                         /*  入力部  */

    leng = strlen(data);                       /*  文字列の大きさを得る  */

    for(i = leng - 1; i >= 0; i = i - 1)       /*  逆順に出力  */
        printf("%c", data[i]);

}

 最後にdo文です。これもほとんど変りませんね。

演習プログラム1−3

/*  Program Exercise1-3  */
/*  Reverse String  */

#include <stdio.h>
#include <string.h>

main()
{
    char data[65535];               /*  データ宣言部  */
    int i, leng;

    scanf("%s", data);              /*  入力部  */

    leng = strlen(data);            /*  文字列の大きさを得る  */

    i = leng - 1;                   /*  逆順に出力  */
    do{
        printf("%c", data[i]);
        i = i - 1;
    }while(i >= 0);
}

 どうでしょうか。だいたいあっていましたか。ちゃんとできているかどうかは、これ以降の段階で明らかになるので、そこで見てもいいのですができれば、組みながらあるいは組んだ後にもう一度見直してみるといいでしょう。

 ここまで組むのに随分時間を掛けてしまいましたが、今までのようなことは非常に重要なのです。あともう少しです。しっかり頑張りましょう。


6)コンパイル──────────────────────────────6)

 できたプログラムをコンパイルしてみましょう。ここで多くの文法の間違いが出てきます。リンクが別手順のものではリンクもしておきましょう。

 ワーニングやエラーはありませんか。間違いがあったら、もう一度エディットし直して、コンパイルしてください。セミコロンはちゃんとついていますか。{}の対応があっていますか。宣言(#include,データ宣言部)は大丈夫ですか。大抵のエラーはこんなところでしょう。

 何もエラー等が出なくなったら次の段階に進みましょう。


7)テスト────────────────────────────────7)

 完成したプログラムを実際に動かして、出力が正しいかどうか確かめてみましょう。

 プログラムによっては動かしてみないと現れないエラーを含んでいることがあるので要注意です。(今回の問題ではそんな事はないでしょうが)

 例えばのプログラム名を reverse.c として作ったとします。これをコンパイルしてreverseと呼び出して走らせてみます。

A>reverse

 ここで何か文字列を入れてリターンキーを押してみましょう。どうですか。ちゃんと反転されていますか。もし旨くいかないようならプログラムの何処が間違っていると思われるので、もう一度見直してみてください。

 また、入力はキーボードからだけではなく、ファイルからも入力できます。この場合は反転させたいファイルをリダイレクションを用いて入力します。

A>reverse < test

 この例はtestというファイルを入力させています。画面にはtestの中身が逆順にでてきます。

 さてここで以下の文字列を入力してみてください。

I Love You.

 どうですか。Iしかでてこないでしょう。実はこれがscanf()の欠点なのです。我々が期待する出力は、

.uoY evoL I

 なのですが、scanf()ではスペースを区切り文字としてしまうため、Iしか入力されないのです。スペースが入っていてもちゃんと文字列全部を認識し、反転してくれるよう改良してみましょう。ではどうすればいいのでしょうか。そうです、scanf()の代りにgetcahr()を用いればいいのですね。


8)改良────────────────────────────────8)

 しかし、getchar()では一文字しか入力できません。一体どうすればいいのでしょうか。少し考えてみてください。ヒントは前の方に書いてあります。









 getchar()を文字列入力に用いる場合、一文字づつ読んできてそれが入力終了記号でない限り、入力を続けるという方法をとります。つまり、

	1)getchar()で一文字読み込む。
	2)入力終了記号ならおしまい。
	3)そうでないなら文字配列にその文字を代入し、
	  配列の添字(制御用変数)を一つ増やし、1)に戻る。
というふうになります。では、ここでいう入力終了記号とは何でしょうか。問題によりますが、ここではEOF(End Of File:ファイルの終わり)かCR(リターンキー)になります。例えば、キーボードから入力する場合リターンキーを押すまでを、一つの文字列と見なすようにするわけです。EOFはファイルからの入力の場合そのファイルが終わるまで(途中にCRがない限り)を一つの文字列と見なすために用います。また、この入力ではループを用いるのですが、大抵の場合は(3種類あるうちの)一種類しか使いません。さっき完成したプログラムでは出力のループが3種類存在したため、プログラムが3つできましたが、今回はそうはいかなのです。なぜでしょうか。少々具体的に、プログラムを考えてみてください。そうすると、うまくいくプログラムでシンプルなものは一つしかないことになります。ちなみにCRは'\n'、EOFはEOFです。









 さて先ほどと同じ様に抽象言語でアルゴリズムを書いてみます。

	制御用変数 = 0;
	一文字 = getchar()
	while((一文字!='\n') && (一文字!=EOF))
	{
	    文字配列[制御用変数] = 一文字
	    制御用変数 = 制御用変数 + 1
	    一文字 = getchar()
	}
 実は慣例的にこのアルゴリズムしかありません。つまりfor文では書かないし、do文ではうまくいきません。(試しに書き換えてみてください。文字配列の中に入力終了記号が入ってしまいます。)

 また、よく見ると制御用変数がそのまま文字列の大きさを表しています。よって実際のプログラムではこの制御用変数はさっきのプログラムのlengを用いればいいでしょう。また、一文字という変数が一つ増えています。これを宣言するのを忘れないで下さい。一文字なのですから型は分かりますね。では実際にさっきのプログラムの入力部分を書き換えてみてください。入力部分だけですよ。また、既にlengには文字列の大きさが入っているので文字列の大きさを得る部分はいりませんので削除しましょう。終わったらコンパイルしてテストしてみましょう。









 では解答です。最初の部分だけ示します。後の部分は変らないのでどれでもお好きなのを使ってください。


演習プログラム1改

#include <stdio.h>                /*  strlen()は使わないのでこれだけでよい  */

main()
    char data[65535];                   /*  データ宣言部  */
    int i, leng;
    char c;

    leng = 0;                           /*  入力と文字列の大きさを得る  */
    c = getchar();
    while((c != '\n') && (c != EOF))
    {
        data[leng] = c;
        leng = leng + 1;
        c = getchar();
    }

    /*  以下出力部分は同じ  */

 どうですか。できましたか。できたらコンパイルしてテストしてみてください。今度は途中にスペースが入ってもちゃんと反転して出力されます。なぜ、scanf()が良くないのかおわかりになったでしょうか。

 さて、とりあえずこれで完成です。と、いいたいのですが、まだですね。このプログラムでは、65536文字以上の大きさの文字列を取り扱うことができません。このプログラムに対する入力が必ず65536文字以下のものであればこれでもいいのですが、どういう場所で使われるか分かりません。練習も兼ねて再帰的に問題を解決するプログラムを書いてみましょう。と、いきたいのですが今回は一つの問題でかなり長く話をしたので今回はこれくらいにしておきましょう。再帰的な問題解決については次回やることにします。また、改良した入力部分を他の文で書き換えて何処がいけないのか、確認してみてください。では、今回はこれで。


CONTENTS / BACK-PAGE / NEXT-PAGE