バイナリデータの入出力

fread()とfwrite()

バイナリデータを読み書きするには、fread() と fwrite() 関数を使う。これらはどんなデータでもバイナリ表現を使って読み書きができる。一般的な形式は次のとおり。

size_t fread(void *バッファ, size_t サイズ, size_t 数値, FILE *ストリーム);
size_t fwrite(void *バッファ, size_t サイズ, size_t 数値, FILE *ストリーム);

fread() は「ストリーム」から「サイズ」バイトの大きさのオブジェクトを「数値」個読み込んで、「バッファ」の指す領域に格納する。戻り値は実際に読み込んだオブジェクトの数。これが「数値」よりも小さければ、エラーが発生したか、ファイルの終わりに達したことになる。どちらかは feof() と ferror() で調べられる。

fwrite() は逆のことを行う。「バッファ」から「サイズ」バイトの大きさのオブジェクトを「数値」個、「ストリーム」に書き込む。戻り値は書き込んだオブジェクト数で、「数値」よりも小さければエラーが発生したということだ。

void型ポインタ

上の、「void *バッファ」というのは、void 型のポインタだ。void 型のポインタは、型変換を行わずに任意の型のデータを指すことのできるポインタで、汎用ポインタ(generic pointer)と呼ばれる。
fread() や fwrite() ではどのような型のデータを扱うかわからないので(違う言い方をすると、どんな型のデータでも扱えるように)、void 型のポインタを使っている。

size_t

size_t 型は stdio.h で定義されている型で、この型の変数はコンパイラがサポートする最大オブジェクトの大きさを持つ値を保持できる。基本の型ではなく size_t 型を使うのは、コンパイラがそれぞれの環境の違いを吸収できるようにするため。

sizof

sizeof 演算子は型、あるいは変数の大きさをバイト数で返す。

sizeof(型);
sizeof 変数;

sizeof を型に対して使うときにはカッコで囲む必要があるけど、変数に対して使うときにはカッコが囲んでも囲まなくてもいい。

例1

次のプログラムは、10 この要素(double 型)をバイナリでファイルに書き込み、開きなおして読み込んで、画面に表示する。当然だけど、ファイルを開くときはバイナリモードで開く必要がある。

#include
#include

double d[10] = {
    10.23,
    19.87,
    1002.23,
    12.9,
    0.897,
    11.45,
    75.34,
    0.0,
    1.01,
    875.875
};

int main(void)
{
    int i;
    FILE *fp;

    /* write array data */
    if ((fp = fopen("myfile", "wb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }

    for (i = 0; i < 10; i++) {
        if (fwrite(&d[i], sizeof(double), 1, fp) != 1) {
            printf("Error: at writing.\n");
            exit(1);
        }
     }
    fclose(fp);

    /* clear array */
    for (i = 0; i < 10; i++) {
        d[i] = -1.0;
    }

    /* read array data */
    if ((fp = fopen("myfile", "rb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }
    for (i = 0; i < 10; i++) {
        if (fread(&d[i], sizeof(double), 1, fp) != 1) {
            printf("Error: at reading.\n");
            exit(1);
        }
    }
    fclose(fp);

    /* display array data */
    for (i = 0; i < 10; i++) {
        printf("%f\n", d[i]);
    }

    return 0;
}
takatoh@nightschool $ ./sample_9_5a
10.230000
19.870000
1002.230000
12.900000
0.897000
11.450000
75.340000
0.000000
1.010000
875.875000

ちなみに myfile ファイルを cat すると、

takatoh@nightschool $ cat myfile
�(\u$@��Q��3@�p=
�Q������)@NbX9��?fffff�&@�(\�R@)\�(�?_�@takatoh@nightschool $

となっている。バイナリだから人間には読めない。

例2

上の例では、配列の要素を1つずつ書き込み・読み込みしていたけど、配列はメモリ上に連続して確保されているので、1つの塊として1度で済ませることもできる。

#include
#include

double d[10] = {
    10.23,
    19.87,
    1002.23,
    12.9,
    0.897,
    11.45,
    75.34,
    0.0,
    1.01,
    875.875
};

int main(void)
{
    int i;
    FILE *fp;

    /* write array data */
    if ((fp = fopen("myfile", "wb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }

    if (fwrite(d, sizeof d, 1, fp) != 1) {
        printf("Error: at writing.\n");
        exit(1);
    }
    fclose(fp);

    /* clear array */
    for (i = 0; i < 10; i++) {
        d[i] = -1.0;
    }

    /* read array data */
    if ((fp = fopen("myfile", "rb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }
    if (fread(d, sizeof d, 1, fp) != 1) {
        printf("Error: at reading.\n"); exit(1);
    }
    fclose(fp);

    /* display array data */
    for (i = 0; i < 10; i++) {
        printf("%f\n", d[i]);
    }

    return 0;
}

30 行目で配列を一気に書き込み、49 行目では一気に読み込んでいる。結果は同じになる。

takatoh@nightschool $ ./sample_9_5b
10.230000
19.870000
1002.230000
12.900000
0.897000
11.450000
75.340000
0.000000
1.010000
875.875000

高レベルテキスト関数

fputs()

fputs() はストリームに文字列を書き込む。

int fputs(char *文字列, FILE *ストリーム);

fputs() は書き込みが成功すると負でない値を返し、エラーが発生すると EOF を返す。文字列の終端にあるヌル文字は書き込まれず、改行文字が追加されることもない。

fgets()

char *fgets(char *文字列, int 数値, FILE *ストリーム);

fgets() はストリームから文字を読み込み、文字列に書き込んでいく。この動作は、

  • 数値 – 1 個分の文字を読む
  • 改行文字に出会う
  • ファイルの終わりに達する

まで続けられる。どの場合にも、文字列の終端にはヌル文字が付け加えられる。
fgets() は成功すると文字列へのポインタを返し、エラーが発生するとヌルポインタを返す。

例1

次のプログラムは、キーボードから入力された文字列を読み込み、ファイルへ保存する。そしてファイルを開きなおして、内容を表示する。

#include
#include
#include

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

    /* check parameters */
    if (argc != 2) {
        printf("Specify file name\n");
        exit(1);
    }

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

    printf("Input string. Empty to quit.\n");
    do {
        printf(": ");
        gets(str);
        strcat(str, "\n");
        if (*str != '\n') {
            fputs(str, fp);
        }
    } while (*str != '\n');
    fclose(fp);

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

    /* read from file */
    do {
        fgets(str, 80, fp);
        if (!feof(fp)) {
            printf("%s", str);
        }
    } while (!feof(fp));
    fclose(fp);

    return 0;
}
takatoh@nightschool $ ./sample_9_4a sample_9_4a.txt
Input string. Empty to quit.
: Hello
: I'm takatoh
: How are you?
: 
Hello
I'm takatoh
How are you?
takatoh@nightschool $ cat sample_9_4a.txt
Hello
I'm takatoh
How are you?

fprintf()とfscanf()

これらは printf() と scnf() のストリーム版だ。コンソールから入出力する代わりに、ストリームから入出力する。

int fprintf(FILE *ストリーム, char *制御文字列, ...);
int fscanf(FILE *ストリーム, char *制御文字列, ...);

例2

次のプログラムは、double 値と int 値と文字列をコマンドラインで指定されたファイルに書き込み、それから、ファイルから読み込みなおして表示する。

#include
#include
#include

int main(int argc, char *argv[])
{
    FILE *fp;
    double ld;
    int d;
    char str[80];

    /* chech parameters */
    if (argc != 2) {
        printf("Specify file name\n");
        exit(1);
    }

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

    fprintf(fp, "%f %d %s", 12345.342, 1908, "hello");
    fclose(fp);

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

    fscanf(fp, "%lf%d%s", &ld, &d, str);
    printf("%f %d %s\n", ld, d, str);
    fclose(fp);

    return 0;
}
takatoh@nightschool $ ./sample_9_4b
Specify file name
takatoh@nightschool $ ./sample_9_4b sample_9_4b.txt
12345.342000 1908 hello
takatoh@nightschool $ cat sample_9_4b.txt
12345.342000 1908 hellotakatoh@nightschool $

書き込んだファイルの最後に改行文字がついてないせいで次のプロンプトが表示されちゃてるけど、画面に表示されるのと同じように、ファイルにも書き込まれているのがわかる。