2000 年度「計算機基礎論3B」 文字を扱うCプログラム

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

「文字列」(string) とは、文字どおり文字の列です。 「文字」(character) は、C言語では char 型と呼ばれる型の変数に納められます。 この「char」は「キャラ」とか「キャル」とか読まれるようです。

文字列は文字が並んだものなので、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 の中だけを変えて、 各行の最後に $ をつけて画面出力するようにしてみました。 これを入力リダイレクトして 「a.out < filename」のように使えば、 ファイルの各行のうしろに余分な空白がないかどうか調べる助けになるでしょう。

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 というソースファイル名をつけてしまいました。 行末にスペースがついているかどうかチェックするプログラムです。 入力リダイレクトを利用して「a.out < filename」と実行すると、 行末にスペースがついている行がファイル filename にあればメッセージを出します。 なければ何も出力されません。 (行末にスペースがついていても何でもないことが多いのですが、 ムダですしあまりカッコよくありません。)

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;
}
===============================================================================

各行を逆転するプログラムです。 「a.out < filename」とするとファイル filename の各行を逆転して (=元が abc だったら cba として) 画面に出力します。 コメントをつけておきましたので読んで考えてください。 文字列の長さを求めるところでは後述の 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);
}                                                                              
===============================================================================

ちょっと実用的な、 キー入力の中で最長の行とその長さを出力するプログラムです。 リダイレクトして「a.out < filename」とすればファイル filename の中で最長の行がわかるでしょう。 ここでは 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 」と出力するプログラムです。

    A   H A P P Y   N E W   Y E A R !
などと書くと文字の大きさに変化をつけなくても (それなりに)強調ができます。 ただし、これは行末にスペースがついてしまう手抜きプログラムです。 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);
    }
}
===============================================================================

アルファベットを大文字にして出力するプログラムです。 入力リダイレクトを使って 「a.out < filename」とすればファイル filename の内容を大文字に変えて画面に出力します。 出力もリダイレクトして 「a.out < filename > filename2」とすればその結果をファイル filename2 に納めます。 大文字化する関数 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 だ。


岩瀬順一