1999 年度「計算機基礎論3B」 1999-11-26

掲示板(WebBBS)が使えるようになりました

最初のページをみてください。

はじめに

きょうの分もどちらかといえばオプション的な内容になります。 前のが終わっていない人は前のに取り組んでください。

標準入力と標準出力

きょうのプログラムもサブディレクトリ ~iwase/prog に置いてありますので、 必要なかたはコピーして利用してください。 また、以下で rev と呼ばれているコマンドは ~iwase/prog/rev.c をコンパイルして得られる実行ファイルを rev と改名したもの、とします。

コマンドの中には 「キーボードから受け取ったデータを処理し、結果を画面に書く」 というタイプのものがあります。 例えば unix に標準でついているコマンド sort はキーボードから受け取った行をソートして (i.e. 辞書式順序に並べかえて)画面に書きます。 rev はキーボードから受け取った各行の中の文字を逆順にして画面に書きます。 行頭で Ctrl+D を打てば 「これでキーボードからの入力はおしまい」 とみなされ、プロンプトに戻ります。

まずはこれらを少し使ってみてください。

※ 打ち込んだものに(いわゆる)日本語が含まれると、 sort は日本語コード順に並べるので必ずしも読みの順にはならない。 rev は出力がぐちゃぐちゃになることがある。これは手抜き。

言い忘れてましたが、引数なしの cat もこの種のコマンドです。

キーボード(から打ち込むデータ)を「標準入力(standard input)」、 画面(に書かれる結果)を「標準出力(standard output)」と呼びます。 cat は「標準入力をそのまま標準出力に送るプログラム」、 sort は「標準入力を行単位で辞書式順序に並べかえて標準出力に送るプログラム」、 rev は「標準入力の各行を逆順にして標準出力に送るプログラム」、 ということになります。

リダイレクトとパイプ

標準入力や標準出力は、下のようにしてファイルに切り換えることができます。 それを 「リダイレクト(redirect)」とか 「リダイレクション(redirection)」と呼びます。 sort や rev でいろいろ試してみるとよいでしょう。 以下しばらく、「prog」で一般のコマンド名を表わすことにします。 この prog に「コマンド名+引数」を代入しても構いません。 たとえば prog が cat filename でもいいのです。

入力リダイレクト

「prog < ifile」とすると、 単に「prog」として起動した後にファイル ifile の内容をキーボードから打ち込んだかのようにプログラム prog が動きます。 これを「入力リダイレクト」といいます。 前もってエディタを使ってデータを打ち込んでおけるので、 データ量の多いときなどに便利です。

例:「sort < file」は file の行をソートして画面に出力します。 「rev < file」は file の各行を逆順にして画面に出力します。

 ※ ifile の内容が prog の期待していたものと違うと、 いつまでもコマンドが終了しない場合がある。 その場合は Ctrl+C を押して中止する。 sort や rev ではそのようなことは起こらないはず。 なお、MS-DOS ではこうなった場合リセットするしかなかった。;_;

※ メールのファイルなどをソートしてみるとつながりがとんちんかんで面白かったりする。:-)

出力リダイレクト

「prog > ofile」とすると、 プログラム prog の出力結果がファイル ofile の内容となります。 画面には書かれません。 これを「出力リダイレクト」といいます。 出力結果をあとからエディタに読み込んでゆっくり調べたり、 編集して再利用したりできるので便利です。

※ プログラムの中で何か質問してくるような場合、 そのメッセージまで ofile に行ってしまって画面には何も出力されない場合がある。

※ この「<」や「>」 は不等号ではなく方向を表わす矢印のようなものである。 「A < B」と「B > A」では意味が全く違う。

※ すでに ofile があった場合は上書きされる。 すなわち、もしもこのコマンドの実行前に ofile が存在すれば、 その内容は失われる。

「prog >> ofile」とすると、 プログラム prog の出力結果がファイル ofile の内容の末尾に追加されます。 画面には書かれません。 ofile がなければ新規に作られるので、 上の「>」の場合と全く同じことになります。 これも「出力リダイレクト」といいます。

例:「ls -la > file」とすれば ls -la した結果 (ディレクトリの一覧)が file の内容となる。 「ls -la >> file」とすれば file の最後に追加される。

「prog < ifile > ofile」や 「prog < ifile >> ofile」 は入力リダイレクトと出力リダイレクトの組合せです。 入力は ifile, 出力は ofile となります。

例:「sort < file >> file2」とすれば file の行をソートした結果を file2 の最後に追加する。

パイプ

「prog1 | prog2」とすると、 prog1 の出力が prog2 の入力になります。 これを「パイプ(pipe)」といいます。

例:「cat file | sort」は file の行をソートして画面に出力する。

※ オプションなしの「ls」はやや特殊で、 「ls | more」とすると「自分の出力はパイプされているぞ!」 と知っていつもとは違った表示のしかたをする。

パイプとリダイレクトの組み合わせも可能です。 「rev < file1 | sort | rev > file2」など。 しかし、この例は 「cat file1 | rev | sort | rev > file2」 と言いかえられますので、 入力リダイレクトとパイプの組み合わせはあまり使わないようです。

文字を扱うCプログラム

そこで、 キーボードから文字を読み込んで処理し、画面に書くCプログラムを書いてみましょう。 上で説明したリダイレクトを利用すればファイルの処理にも使えます。

なお、文字(特に文字列)を扱うには 「ポインタ」と呼ばれる概念が不可欠なのですが、 初心者はポインタでつまずきがちなので、 まずはポインタを使わずに書けるプログラムだけを扱います。 また、教科書では getchar(), putchar() といった、 一文字ずつ入出力を行なう関数から導入することが多いようですが、 今年度は試しに gets(), puts() といった文字列入出力関数から始めてみます。

「文字列」(string) とは、文字どおり文字の列です。 文字は、C言語では char 型と呼ばれる型の変数に納められます。 いままでに出てきた int 型と double 型に char 型が仲間として加わったわけです。 この char は「文字」(character) の最初のほうをとったものなので、 「キャラ」とか「キャル」とか呼ぶようです。

文字列は文字が並んだものなので、char が並んだもの、 すなわち char 型の配列の格納されます。 ただし、文字列は長さがまちまちなので、 十分大きな配列に格納し、 文字列の終わった次の要素には '\0' という特別な文字をいれておく、という約束になっています。

たとえば "Hello, world\n" という文字列を char a[80+1]; と宣言された配列に納めると a[0] が 'H', a[1] が 'e', a[2] が 'l', ..., a[10] が 'l', a[11] が 'd', a[12] が '\n' となり、a[13] が '\0' となります。

    +------+------+------+-)   )-+------+------+------+------+------+--)    )-+------+
    |  H   |  e   |  l   |(...(  |  l   |  d   |  \n  |  \0  | 不定 | ( ...(  | 不定 |
    +------+------+------+-)   )-+------+------+------+------+------+--)    )-+------+
      a[0]   a[1]   a[2]          a[10]  a[11]  a[12]  a[13]  a[14]     ...    a[80]

※ 80+1 と書いたのは“'\0' を除いて 80 文字まで格納できる” と強調したかったため。

※ C言語では、 文字列は二重引用符「"」で、 文字は一重引用符「'」(アポストロフと同じ記号)で囲む。 \n や \0 は二文字で文字一文字を表わすので「'」で囲むのだ。

a[14] から a[80] までに何がはいっていても、 文字列としては "Hello, world\n" であることに変わりありません。

さて、gets() は char 型の配列名を引数にとる関数で、 キーボードからの入力を待ち、その配列にキー入力の一行分を読み込みます。 返り値はまだ習っていない char へのポインタというものなのですが、 いまは「NULL が返ったらキー入力の終わり」と思っておけば十分です。 この「NULL」もいまは説明しません。 一行入力したらリターンを押しますが、 そのリターンは配列に納められません。 gets() と対になるのが puts() です。 この関数も char 型の配列名を引数にとる関数で、 その配列に格納されている文字列を画面に書き、改行を行ないます。 くわしくは次のプログラムを使って説明しましょう。

== mycat.c ====================================================================
#include <stdio.h>

char buf[4096+1];   /* 4096 文字分、+1 は '\0' のため */

main() {
    while (gets(buf) != NULL) {
        puts(buf);
    }
}
===============================================================================

これは cat の機能の一部だけをまねしたプログラムで、 キーボードから入力された行をそのまま画面に書きます。

char 型の配列は名前を buf としました。 データをいったん格納するところをバッファ (buffer) と呼ぶことがあるので、 それにちなんだ名前です。 大きさは 4096+1 にしました。 コメントにもつけておいたように、これで(一行あたり)4096 文字まで扱えます。 普通のエディタの画面が横 80 文字ですから、 画面で約 50 行分が一行になっていても OK というわけです。 たいていの場合、これで大丈夫でしょう。 なお、4096 を選んだのは 2 のベキだからで、深い意味はありません。 この配列は大きいので、main の中カッコの外(main の前)に置きました。

プログラム本体は3行だけです。 gets(buf) とあるのでプログラムはいったん止まってキー入力を待ちます。 ここでユーザがたとえば Hello, world と打ってリターンを押したとすると、 配列 buf に "Hello, world" と格納され、その直後に '\0' が入ります。 いまはキー入力の終わりではなかったので返り値は NULL ではありません。 よって while ループの中が実行され、 配列 buf の中味が画面に出力されます。 すなわち、buf[0] から始まって一文字ずつ画面に出力されます。 出力は最初に '\0' に出会ったところで止まり、そのあとで改行します。 これを何回かくりかえしたところでユーザが 「これでおしまい」という意思表示のため行頭で Ctrl+D を押すと、 gets(buf) の返り値は NULL となります。 すると while ループから脱出するのでプログラムは終わります。

……ということなのですが、 細かい理屈はなかなか一度にはわからないでしょう。 全体を一つのパターンとして理解すればいいと思います。

== mycat2.c ===================================================================
#include <stdio.h>

char buf[4096+1];

main() {
    while (gets(buf) != NULL) {
        printf("%s$\n", buf);
    }
}
===============================================================================

while の中だけを変えて、 各行の最後に $ をつけて画面出力するようにしてみました。 これを入力リダイレクトして使えば、 ファイルの各行のうしろに余分な空白がないかどうか調べる助けになるでしょう。

printf を使って文字列を出力させるには、 " ... " の間には %s と書いておき、 うしろに(char 型の)配列名を書きます。 %s はその配列に格納された文字列で置き換わって出力されます。 printf を使った場合は \n を書いて明示的に指定しない限り改行はしません。

※ printf("Hello, world\n") のように printf には文字列を渡すことができるのだから

    printf("%s$\n", buf);
の代わりに
    printf(buf);
    printf("$\n");
とできそうだが、 配列 buf に格納された文字列に %d だの %s だのが含まれているとまずいのだ。

== mycat3.c ===================================================================
#include <stdio.h>

char buf[4096+1];

main() {
     int i;

     while (gets(buf) != NULL) {
         for (i = 0; buf[i] != '\0'; i++) {
             ;
         }
         if (i > 0) {
             if (buf[i-1] == ' ') {
                 printf("「%s」の行は行末にスペースがついています.\n", buf);
             }
         }
     }
}
===============================================================================

実はもう cat とはだいぶ関係なくなってきたのですが、 手抜きで mycat3.c というソースファイル名をつけてしまいました。 for ループでは、buf に読み込まれた文字列の後ろの \0 をさがしています。 ループの本体が空のときは上のようにセミコロンだけを書きます。 そのあとの if では文字列が空(i.e. buf[0] が \0)の場合を除外してから \0 の直前がスペースかどうかチェックしています。 「スペースという文字」は ' ' のように一重引用符の間にスペースを書いて表わします。

※ i が 0 のときに buf[i-1] == ' ' のチェックを行なうと配列の外を調べることになり、 正しくないプログラムになってしまう。 よって上のように書いたのだ。 実は、C言語の約束により、 if ((i > 0) && (buf[i-1] == ' ')) と書いても i > 0 の場合のみ buf[i-1] == ' ' のチェックを行なうので正しいプログラムとなるのだが、 上の例ではそういった細かい点に踏み込まないように書いてみた。

== rev.c ======================================================================
#include <stdio.h>

char buf1[4096+1];  /* 入力した文字列用 */
char buf2[4096+1];  /* それを逆転した文字列用 */

int main() {
    int i, len;

    while (gets(buf1) != NULL) {
        for (len = 0; buf1[len] != '\0'; len++) {
            ;
        }
        len--;                          /* len =「文字列の長さ−1」 */
        for (i = 0; i <= len; i++) {    /* 逆順にコピー */
            buf2[i] = buf1[len-i];
        }
        buf2[i] = '\0';                 /* 最後の '\0' を忘れてはいかん */
        puts(buf2);                     /* 出力 */
    }
    return 0;
}
===============================================================================

最初に使ってもらった、各行を逆転するプログラムです。 コメントをつけておきましたので読んで考えてください。 文字列の長さを求めるところでは後述の strlen() を使っても構いません。 入力された文字列の長さが 0 のとき、 二つめの for にはいる直前で len は -1 になってしまいますが、 その場合は二つめの for を一度も回わらないので(かろうじて) 正しく動くはずです。 もっとうまい書き方は K&R2 に出ていますので見てください。

== maxlen.c ===================================================================
#include <stdio.h>
#include <string.h> /* strlen(), strcpy() */

char buf1[4096+1];  /* キー入力を納める */	
char buf2[4096+1];  /* 今までの最長の行を納める */

main() {
    int max;    /* 今までの最長の長さ */
    int len;	/* 今の行の長さ */

    max = 0;
    buf2[0] = '\0';
    while (gets(buf1) != NULL) {
        len = strlen(buf1);
        if (len > max) {
            max = len;
            strcpy(buf2, buf1);
        }
    }
    printf("最長は「%s」の %d 文字です.\n", buf2, max);
}                                                                              
===============================================================================

ちょっと実用的な、 キー入力の中で最長の行とその長さを出力するプログラムです。 ここでは strlen(), strcpy() という文字列処理用の備え付けの関数を使いました。 これらを使うには string.h を #include する必要があります。 前者は文字列の長さ(\0 は含めない)を返します。 後者は文字列を複写します。複写元が第二引数、複写先が第一引数です。 複写先は十分大きな配列でなければなりません。

== sukima.c ===================================================================
#include <stdio.h>

char buf[4096+1];

main() {
    int i;

    while (gets(buf) != NULL) {
        for (i=0; buf[i]!='\0'; i++) {
            putchar(buf[i]); putchar(' ');
        }
        putchar('\n');
    }
}
===============================================================================

「abc」と打ったら「a b c 」と出力されるプログラムです。 putchar() は char 型変数を引数にとり、 その文字を画面に書く関数です。 putchar('\n') とすれば改行します。

== toupper.c ==================================================================
#include <stdio.h>
#include <ctype.h>  /* toupper() */

char buf[4096+1];

main() {
    int i;

    while (gets(buf) != NULL) {
        for (i=0; buf[i]!='\0'; i++) {
            buf[i] = toupper(buf[i]);
        }
        puts(buf);
    }
}
===============================================================================

アルファベットを大文字にして出力するプログラムです。 大文字化する関数 toupper を使いました。 char 型を引数にとり、 それがアルファベット小文字の場合、またその場合に限り、 それを大文字化した文字を返します。 それ以外の場合は引数をそのまま返します。 toupper を使うには ctype.h の #include が必要です。

== kaiwa.c ====================================================================
#include <stdio.h>

char buf[128+1];

main() {
    do {
        printf("お名前をどうぞ。\n");
    } while ((gets(buf) == NULL) || (buf[0] == '\0'));
    printf("%s さんですね、こんにちは!\n", buf);
}
===============================================================================

あまり意味があるとも思えない、 会話プログラムです。 いままで説明していなかった do-while ループを使ってみました。

    do {
        ...
    } while (式);
は次のように動きます。

※ キーワード while を見かけたら while ループか do-while ループかが問題になるが、 いままで説明してきたインデントに従う限り、 すぐわかる。 前に閉じ中カッコがあれば do-while, そうでなければ while だ。

nkf

nkf は漢字コード変換を行なうプログラムです。 ここでいう「漢字」とは全角文字のことだと思ってください。 「漢字コード」の代わりに「日本語コード」ということもあります。 「nkf -j」「nkf -s」「nkf -e」 はそれぞれそのあとの引数で指定されたファイルの内容を 「JIS 漢字」「シフト JIS」「日本語 EUC」に変換して標準出力に送ります。

実習用 WS が採用している漢字コードは日本語 EUC です。 MS-Windows や MS-DOS はシフト JIS を使っています。 メールは JIS 漢字で送るのが決まりです。

grep

メールを収めたファイルが多くなってくると、 「あのメールはどのファイルにはいってたっけ」ということが起こります。 そんなときには grep が便利です。

「grep test *」とすると、カレントディレクトリのすべてのファイル (ただし「.」で始まる名前のものを除く)のなかから test という語句をさがし出します。 「grep test 9911*」だと 9911 で始まるファイル名のファイルだけからさがします。


岩瀬順一 <iwase@kappa.s.kanazawa-u.ac.jp>