練習問題9.3

1. コマンドラインで指定されたファイル(テキストまたはバイナリ)のバイト数を数え、その結果を表示するプログラムを作成してください。

#include
#include

int main(int argc, char *argv[])
{
    FILE *fp;
    long count = 0;
    char ch;

    if (argc != 2) {
        printf("Usage: %s \n", argv[0]);
        exit(1);
    }

    if ((fp = fopen(argv[1], "rb")) == NULL) {
        printf("Channot open file: %s\n", argv[1]);
        exit(1);
    }

    while (!feof(fp)) {
        ch = fgetc(fp);
        if (ferror(fp)) {
            printf("Error: at reading file.\n");
            exit(1);
        }
        count++;
    }

    if (fclose(fp) == EOF) {
        printf("Error: at closing file.\n");
        exit(1);
    }

    printf("%ld bytes\n", count - 1);

    return 0;
}

出力部分で – 1 しているのは、EOF も数えてしまっているから、それを差し引いている。

実行例:

takatoh@nightschool $ ./practice_9_3_1 sample_9_3
8925 bytes
takatoh@nightschool $ ls -l sample_9_3
-rwxrwxr-x 1 takatoh takatoh 8925  5月 10 19:11 sample_9_3

feof()とferror()

fgetc() が EOF を返した場合、エラーが起こったのかファイルの終わりに達したのか判定できない。バイナリファイルを読み込んでいる途中で EOF と同じ値が返ってきた時もファイルの終わりなのか有効なデータなのか判定できない。
これらを判定するためには、feof() と ferror() を使う。一般的な形式は次のとおり。

int feof(FILE *ストリーム);
int ferror(FILE *ストリーム);

feof() はストリームの終わりに達していると 0 以外の値(真)を返し、そうではければ 0 (偽)を返す。
ferror() はエラーが起これば 0 以外の値を返し、そうでなければ 0 を返す。

FILE *fp;
.
.
.
while (!feof(fp)) {
    ch = fgetc(fp);
    if (ferror(fp)) {
        printf("File error!\n");
        break;
    }
}

上のコードでは、feof(fp) が偽のあいだ while でループし、ストリームからデータを読み込むたびに ferror(fp) でエラーチェックをしている。

次のプログラムはファイルのコピーを行う。その際、完全なエラー検査を行う。

#include
#include

int main(int argc, char *argv[])
{
    FILE *from, *to;
    char ch;

    if (argc != 3) {
        printf("Usage: %s \n", argv[0]);
        exit(1);
    }

    /* open src file */
    if ((from = fopen(argv[1], "rb")) == NULL) {
        printf("Cannot open src file.\n");
        exit(1);
    }

    /* open dest file */
    if ((to = fopen(argv[2], "wb")) == NULL) {
        printf("Cannot open dest file.\n");
        exit(1);
    }

    /* copy file */
    while (!feof(from)) {
        ch = fgetc(from);
        if (ferror(from)) {
            printf("Error: at reading file.\n");
            exit(1);
        }
        if (!feof(from)) {
            fputc(ch, to);
        }
        if (ferror(to)) {
            printf("Error: at writing to file.\n");
            exit(1);
        }
    }

    if (fclose(from) == EOF) {
        printf("Error: at closing src file.\n");
        exit(1);
    }

    if (fclose(to) == EOF) {
        printf("Error: at closing dest file.\n");
        exit(1);
    }

    return 0;
}

実行例:

takatoh@nightschool $ ./sample_9_3
Usage: ./sample_9_3  
takatoh@nightschool $ ./sample_9_3 myfile myfile2
takatoh@nightschool $ cat myfile2
This is a test for file system.

ファイルシステムの基礎

ファイルシステムというか、ファイル入出力の基礎。

fopen()

ファイルを開くとき、つまりファイルをストリームと結びつけるときには fopen() を使う。fopen() の一般的な形式は次のとおり。

FILE *fopen(char *ファイル名, char *モード);

本(「独習 C」)に習ってプロトタイプ風に書いてみた。FILE * は戻り値の型を示していて、FILE という構造体へのポインタだ。
fopen() をはじめ、ファイルシステム関連の関数は stdio.h を使う。FILE も stdio.h で定義されている。
モードは、書き込みか読み込みか、あるいはテキストかバイナリかなどを示す文字列(へのポインタ)だ。次のようなモードがある。

r読み込み用にテキストファイルを開く
w書き込み用にテキストファイルを開く
aテキストファイルに追加する
rb読み込み用にバイナリファイルを開く
wb書き込み用にバイナリファイルを開く
abバイナリファイルに追加する
r+読み込み/書き込み用にテキストファイルを開く
w+読み込み/書き込み用にテキストファイルを作成する
a+読み込み/書き込み用にテキストファイルに追加する。またはテキストファイルを作成する。
r+b読み込み/書き込み用にバイナリファイルを開く
w+b読み込み/書き込み用にバイナリファイルを作成する
a+b読み込み/書き込み用にバイナリファイルに追加する。またはバイナリファイルを作成する。

ファイルを開く操作が失敗すると、fopen() はヌルポインタを返す。stdio.h では NULL というマクロがヌルポインタとして定義されている。ファイルを開いた時には、それが成功したかどうかをチェックしなければいけない。

FILE *fp;

if ((fp = fopen("myfile", "r")) == NULL) {
    printf("Channot open the file.");
    exit(1);
}

fclose()

ストリームが不要になった時にはこれを閉じる必要がある。ストリーム(ファイル)を閉じるには fclose() を使う。

int fclose(FILE *ストリーム);

文字の読み込みと書き込み

文字の読み込みには fgetc()、書き込みには fputc() をつかう。

int fgetc(FILE *ストリーム);
int fputc(int 文字, FILE *ストリーム);

fgetc() はストリームの次のバイトを unsigned char として読み込み、int として返す。エラーが起こったり、ファイルの終わりに達すると EOF を返す。fgetc() は int を返すけど、読み込んだ文字は下位バイトに含まれるので char に代入することもできる。
fputc() はストリームに「文字」の下位バイトを unsigned char の値として書き込む。「文字」は int と定義されているけど、char を渡しても問題ない。書き込みが成功すると書き込んだ文字を返し、失敗すると EOF を返す。

次のプログラムは、文字列をテキストファイル myfile に書き込み、いったん閉じたあと読み込み用に開きなおして、内容を表示する。

#include
#include

int main(void)
{
    char str[80] = "This is a test for file system.\n";
    FILE *fp;
    char ch, *p;

    /* open myfile to write */
    if ((fp = fopen("myfile", "w")) == NULL) {
        printf("Channot open the file.\n");
        exit(1);
    }

    /* write str. */
    p = str;
    while (*p) {
        if (fputc(*p, fp) == EOF) {
            printf("Error.\n");
            exit(1);
        }
        p++;
    }
    fclose(fp);

    /* open myfile to read */
    if ((fp = fopen("myfile", "r")) == NULL) {
        printf("Channot open the file.\n");
        exit(1);
    }

    /* read the file */
    for ( ; ; ) {
        ch = fgetc(fp);
        if (ch == EOF) {
            break;
        }
        putchar(ch);
    }
    fclose(fp);

    return 0;
}

実行結果:

takatoh@nightschool $ ./sample_9_2
This is a test for file system.

練習問題9.2

1. コマンドライン引数で指定したテキストファイルの内容を表示するプログラムを作成してください。

#include
#include

int main(int argc, char *argv[])
{
    FILE *fp;
    char ch;

    if (argc != 2) {
        printf("Usage: %s \n", argv[0]);
        exit(1);
    }

    if ((fp = fopen(argv[1], "r")) == NULL) {
        printf("Cannot open file: %s\n", argv[1]);
        exit(1);
    }

    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }
    fclose(fp);

    return 0;
}
takatoh@nightschool $ ./practice_9_2_1
Usage: ./practice_9_2_1 <textfile>
takatoh@nightschool $ ./practice_9_2_1 myfile
This is a test for file system.

ストリームとファイル

C の入出力システムでは、プログラムと実際のデバイスとのあいだに抽象的なレベルを設けている。これをストリームという。ストリームのおかげで、プログラムからは実際のデバイスにかかわらずほとんど同じように扱える。
これに対して、実際のデバイスをファイルと呼ぶ。この文脈ではいわゆる普通のファイルだけでなく、画面、キーボード、ポートなどを含んでいる。

ストリームには、テキストストリームとバイナリストリームがある。
テキストストリームは ASCII 文字を扱う。テキストストリームでは、何らかの文字変換が行われることがある。例えば、改行文字を出力する場合には大抵復帰改行に変換される。なので、ストリームに送られるデータとファイルに書き込まれるデータが一致するとは限らない。
一方、バイナリストリームはどんな形式のデータでも扱うことが出来、文字変換も行わない。ストリームに送られれデータとファイルに書き込まれるデータは一致する。

もうひとつ、現在位置(current possition)という概念がある。現在位置は次のファイルアクセスが起こる位置を示している。例えば、100バイトのファイルがあり、すでに半分読み込んでいるとすると、次のアクセスでは 50 バイトめ(51バイトめ?)であり、これが現在位置ということになる。

ふむ、まあ、何とかわかった。次はいよいよファイルアクセスだ。