C言語編 第42章 バイナリファイルの読み書き

先頭へ戻る

この章の概要

この章の概要です。

バイナリファイル

前章までに扱ってきたファイルは、文字だけで構成されていました。 このようなファイルは、テキストファイル(テキスト形式ファイル)と呼ばれます。 これに対して、この章で扱う、数値の羅列で表現されたファイルを、 バイナリファイル(バイナリ形式ファイル)と呼びます。

バイナリファイルは、文字、画像や音声、動画といった様々な形式のデータを扱うことができます。 文字も扱えるので、テキストファイルをバイナリファイルとみなして扱うことも可能です。 その場合、何か違うかといえば、改行の扱いが違います。

テキストファイルとして読み書きを行う場合、改行文字を本当に "改行" の意味で扱いますが、 バイナリファイルの場合は単なる数値でしかありません。 そもそも、テキストだけを扱うファイル形式ではないので、「行を変える」という感覚自体がありません。
改行文字に関する話は、後で改めて取り上げます

なお、環境によっては、そもそもテキストファイルとバイナリファイルという区別が無いものもあります。

バイナリデータを確認する

まずは、イメージを掴むためにも、バイナリファイルの中身を実際に見てみることにしましょう。 テキストファイルをテキストエディタで確認できるように、 バイナリファイルは、バイナリエディタを使って確認できます。

「エディタ」は編集者ということなので、書き換えを行いそうな感じですが、 バイナリエディタの場合は、あまり書き換えることは多くないです。 テキストと違って、簡単には読み解けないことが多いので、自由に思い通りに書き換えるのはなかなか難しいです。

バイナリエディタは、無料で簡単に入手できるので、ダウンロード&インストールをしておくと良いです。 何を使っても構いませんが、ここでは DANDP Binary Editor(作者様のサポートページ)を使ってみます。ダウンロードから、インストールまでの流れについては割愛します。macOS ならば、HexEdit(公式プロジェクト)などがあります。

後ほど作成するサンプルプログラムによって生成される test.bin というバイナリファイルを読み込ませると、次のように表示されます。 test.bin は、「900」という int型の整数値、「7.85」という double型の浮動小数点数、「"xyzxyz"」という文字列を書き込んだバイナリファイルです。

DANDP Binary Editor での表示

左端の列に、アドレスが表示されています。 ここでいうアドレスはメモリアドレスではなく、ファイルの先頭を 0 とした、ファイル内のアドレスです。 ファイルポジション(第40章)が保持している情報はこれだと考えると良いでしょう。

その右側には、バイナリデータを 16進数で表記した羅列が表示されています。 「900」とか「7.85」といった数値が書き込まれているファイルですが、バイナリエディタで見るとこのように、 元が何であったか分からないような表示になります。 きちんと法則、というか表現形式が定まっているものなので、知識があれば読み解くことができます。

右端には、各バイトを、文字として表現したらどうなるかが表示されています。 これは、ASCIIコードと呼ばれる文字の表現形式が使われているのですが、この話題は後述します
ともかく、各バイトを文字化しているだけなので、「900」という整数値だったものが、「9」「0」「0」と並んでいる訳ではありません。 int型の「900」は、4バイト(環境によります)の大きさを使って、ある表現形式に従って記録されているので、 バイナリファイルの中には、「9」「0」「0」のような数字の並びは登場しません。

そこで、アドレス 00000000~00000003 の 4バイト分を見てみましょう。 ここに「900」が記録されています。 16進法で、84030000 となっていることが分かります。

まず、900 という 10進数が、16進数で幾つになるか調べてみます(手作業での変換については第19章で確認しました)。 すると、0x384 であることが分かります。 これがどうして、84030000 という表示になってしまうのかは、しっかり理解しておく必要があります。

この理解のためには、エンディアン(あるいはバイトオーダー)という考え方を知る必要があります。 これは、2バイト以上あるデータをメモリ上に配置するとき、各バイトをどのように並べるのかというルールのことです。 現在、圧倒的多数を占めている方式は、リトルエンディアンビッグエンディアンという2つの方式です。

今回の、0x384 という数値は、1バイト単位に分解すると、0x03 と 0x84 に分けられます。 実際には、4バイトの数値としてファイルへ書き出したので、頭に更に2バイト分の 0x00 があるはずです。 つまり、「0x00 0x00 0x03 0x84」です。

これに対し、バイナリエディタ上に表示されているのは、「0x84 0x03 0x00 0x00」なので、どうやら並びが変わっているだけのようです。 このように、「0x00 0x00 0x03 0x84」という順番のデータを、逆の順番「0x84 0x03 0x00 0x00」のように並べる方式は、リトルエンディアン方式です。 もし、ビッグエンディアン方式であれば、「0x00 0x00 0x03 0x84」は、そのままの順番「0x00 0x00 0x03 0x84」で並びます

Windows や macOS が動くコンピュータは、Intel系の CPU を使用しており、Intel が採用している方式がリトルエンディアンであることから、 現在では多くの場合、リトルエンディアンになっています。 他の環境や、使用用途によっては、ビッグエンディアン方式を使っている可能性もあるので、 多様な使い方をするデータ、プログラムでは、エンディアンの違いを考慮しなければなりません。

例えば、エンディアンが異なるコンピュータ同士をネットワークでつないで、データをやり取りするような場合、 データの格納順序の違いを吸収しなければなりません。

一見、リトルエンディアン方式は素直でないように見えるかもしれませんが、実際には数値の下位の桁ほど、 若いアドレスに配置されているのですから、そういう視点で見ると素直な並びであるともいえます。 実際、そのおかげで、4バイトの整数を 2バイトに切り詰めるような処理は効率的に実現できます(上位のアドレスにあるデータを単に無視するだけで良い)。
この手の処理は、ビッグエンディアン方式の方がずっと面倒なことになります。

文字コード

今度は文字列がどう表現されるのかについて説明します。

先ほどのバイナリエディタの表示で、右端のテキスト表記の部分をみると、"xyzxyz" という文字列が確認できます。 アドレスにすると、0000000C~00000011 に当たりますが、この部分の 16進数の表示は「78 79 7A 78 79 7A」となっています。 ここから、'x' という文字は 78 であり、'y' は 79、'z' は 7A だと分かります。
これは次のようなプログラムで試してみても分かります。

#include <stdio.h>

int main(void)
{
    printf( "%X\n", 'x' );
    printf( "%X\n", 'y' );
    printf( "%X\n", 'z' );

    return 0;
}

実行結果(標準出力)

78
79
7A

つまり、文字のデータであっても、内部的には、何らかの数値として表現されていることが分かります。 文字と数値との対応関係は、文字コードという考え方で取り決められています。

文字コードには様々な種類があるため、異なる文字コードを使っていると、同じ 'x' という文字でも、数値化したときの値は異なる可能性があります。 Webサイトやメールなどで、文字化けが起こることがあるのは、これが1つの原因です。

多くのバイナリエディタでは、テキスト形式で表示される部分には ASCIIコードが使われます。 ASCIIコードは、7ビットで 1文字を表現する形式になっています。 7ビットということは、最大で 128種類の文字しか表現できない訳ですから、日本語の表示など到底不可能です (ASCII の「A」は「American」なので、そもそも英語圏で必要が無い文字のことは考えられていないのです)。
実際、ASCIIコードは、半角英数字と、少しの記号類、幾つかの制御文字が含まれているだけであり、日本語の表現に関わるものは何も含まれていません。ASCIIコード表は、至る所に掲載されている(⇒Wikipedia)ので、ざっと眺めておくと良いでしょう(暗記する必要はありません)。

そのため、ファイルに書き出した文字列が "xyzxyz" のような、ASCIIコードに含まれている文字であれば、バイナリエディタ上でそのまま表示されます。 しかし、ASCIIコードに含まれない文字は、バイナリエディタ上では、普通には読めない形で表示されるはずです。

C言語のプログラムでは、"日本語" のような文字列も、fputs関数などを使ってファイルへ書き出すことがあります。 これは、日本語が扱える環境では、ASCIIコードとは違う、日本語の文字を含んだ文字コードが使われているからです。 この辺りの話は長くなるので、第46章で改めて取り上げます。

なお、先ほどの "xyzxyz" という文字列の直後に、テキスト表現だと '.' 、16進数だと 00 という文字があります。 これは、test.bin を書き出す際に、文字列の終端にある '\0' も書き出したため存在するものです。 つまり、'\0' という終端文字の正体は 00 という数値です。 テキスト表現が '.' となっているのは、何らかの表示可能な文字として表現できない文字を、'.' で代替するということに(このバイナリエディタが)しているからです。

fwrite関数による書き込み

では、バイナリファイルの読み書きを行っていきましょう。 まずは書き込みを試します。 次項では、作成されたバイナリファイルを読み込む実験を行います。

ファイルを、バイナリファイルとして開くには、fopen関数に指定するオープンモードに、"b" を含むものを指定します。この "b" は、バイナリ(Binary) の b です。具体的には、以下のものがあります。

オープンモード 意味
rb バイナリファイルを読み込み用に開く
wb バイナリファイルを書き込み用に開く
ab バイナリファイルを追記用に開く
rb+ または r+b バイナリファイルを読み書き両用に開く
wb+ または w+b バイナリファイルを読み書き両用に開く
ab+ または a+b バイナリファイルを読み書き両用で追加あるいは作成する

"r"、"w"、"a" の意味合いはこれまでの章で見てきたとおりです。 つまり、以下のようになっています。

rb wb ab rb+ または r+b wb+ または w+b ab+ または a+b
読み取り できる できない できない できる できる できる
書き込み できない できる できる できる できる できる
開くとファイルの中身は… そのまま 失われる そのまま そのまま 失われる そのまま
開くとファイルポジションは… 先頭にある 先頭にある 終わりにある 先頭にある 先頭にある 終わりにある
ファイルが存在しないときに開こうとすると… 失敗する 空のファイルが作られる 空のファイルが作られる 失敗する 空のファイルが作られる 空のファイルが作られる

書き込み自体は、fwrite関数で行います。幾つも書き込み関数の種類があったテキストファイルと違って、バイナリファイルを書き込む標準ライブラリ関数は、これしかありません。fwrite関数は、stdio.h に次のように宣言されています。

size_t fwrite(const void* ptr, size_t size, size_t n, FILE* stream);

第1引数に、書き込みたいデータのメモリアドレスを指定します。 これは配列の要素のメモリアドレスであっても構いません。

第2引数は、書き込むデータ1つ分の大きさを指定します。 例えば、int型の変数の値を1つだけ書き込むのであれば「sizeof(int)」のように、 配列をまとめて書き込む場合も、ここには1個分の大きさを指定するので、「sizeof(array[0])」のようにすると良いです。

第3引数は、書き込むデータの個数を指定します。 第1引数に指定したメモリアドレスが配列の場合に、ここに要素数を指定できます。 単独のデータであれば 1 を指定します。

第4引数は、書き込み先ストリームの指定です。

戻り値は、実際に書き込まれたデータの個数が返されます。 何らかのエラーが起きれば、第3引数の n に指定した値よりも小さい値が返されます。

それでは実際に試してみます。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE* fp;
    int num = 900;
    double d = 7.85;
    char str[] = "xyzxyz";


    fp = fopen( "test.bin", "wb" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    fwrite( &num, sizeof(num), 1, fp );
    fwrite( &d, sizeof(d), 1, fp );
    fwrite( str, sizeof(str[0]), sizeof(str), fp );

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

実行結果(標準出力)


実行結果(test.bin のテキスト表現)

????ffffff@xyzxyz 

ファイルの拡張子 ".bin" は、バイナリ(Binary) を表す一般的な拡張子です。 よく使われますが、どんな意味合いのデータなのかはよく分からない不明瞭な拡張子ではあります。

test.bin の中身は、テキストでは表現できない状態になっているので、実行結果上では「?」としました。 バイナリエディタで開いてみると、前の項の写真と同じ結果になっているはずです。

3度の fwrite関数の呼び出しによって、int型の値を1つ、double型の値を1つ、6文字の文字列(ヌル文字を含めて7文字)を1つ書き込んでいます。文字列を書き込むとき、fwrite関数の第3引数を「sizeof(str)」としているため、ヌル文字も含まれます(第32章)。バイナリデータなので、C言語の文字列のルールを無視して、ヌル文字を付けずに書き出しても、読み込む際に気を付ければ別に問題はありません。ヌル文字が不要なら -1 するか、strlen関数に置き換えます。

fread関数による読み込み

次に、バイナリファイルの読み込みを行ってみましょう。 先ほどのサンプルプログラムで作成されたファイルを読み込みます。

バイナリファイルの読み込みには、fread関数を使います。書き込み同様、バイナリファイルの場合の読み込み関数は、これしかありません。 fread関数は、stdio.h に以下のように宣言されています。

size_t fread(void* ptr, size_t size, size_t n, FILE* stream);

第1引数に、読み込んだデータを格納する変数のメモリアドレスを指定します。 これは配列の要素のメモリアドレスであっても構いません。

第2引数は、読み込みデータ1つ分の大きさを指定します。 例えば、int型の変数の値を1つだけ読み込むのであれば「sizeof(int)」のように、 配列をまとめて読み込む場合も、ここには1個分の大きさを指定するので、「sizeof(array[0])」のようにすると良いです。

第3引数は、読み込みデータの個数を指定します。 第1引数に指定したメモリアドレスが配列の場合に、ここに要素数を指定できます。 単独のデータであれば 1 を指定します。

第4引数は、読み込み元ストリームの指定です。

戻り値は、実際に読み込まれたデータの個数が返されます。 何らかのエラーが起きたとき、あるいは、ファイルの終わりに達していた場合には、 第3引数の n に指定した値よりも小さい値が返されます。

では試してみましょう。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE* fp;
    int num;
    double d;
    char str[7];


    fp = fopen( "test.bin", "rb" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    if( fread( &num, sizeof(num), 1, fp ) < 1 ){
        fputs( "読み込み中にエラーが発生しました。\n", stderr );
        exit( EXIT_FAILURE );
    }
    if( fread( &d, sizeof(d), 1, fp ) < 1 ){
        fputs( "読み込み中にエラーが発生しました。\n", stderr );
        exit( EXIT_FAILURE );
    }
    if( fread( str, sizeof(str[0]), sizeof(str), fp ) < sizeof(str) ){
        fputs( "読み込み中にエラーが発生しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    printf( "%d\n", num );
    printf( "%f\n", d );
    printf( "%s\n", str );

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

実行結果(標準出力)

900
7.850000
xyzxyz

fread関数で文字列を読み込む場合、ヌル文字の扱いに注意して下さい。 fread関数は、文字列を読み込んだからといって、自動的にヌル文字を付加することはありません。 そもそも、バイナリデータを扱う関数なので、文字列なのかどうかを気にしていません。
普通、読み込んだ文字列は、そのあとの処理の中で文字列として扱うはずですから、ヌル文字が付いていないと困ります。 バイナリファイル側にヌル文字も書き込まれているのなら、それごと読み込めばよいですし、 書き込まれていないのなら、読み取った後で付け足すなどしなければなりません。

考えてみると、読み込むファイルの中身が、 「int型の数値、double型の数値、ヌル文字込みで 7文字の文字列が並んでいる」ことが知っていないと、 このプログラムを書けないことにお気づきでしょうか?
バイナリファイルの読み込みは、どんなファイルフォーマットになっているのか知っていないと実装できません。 とりあえず 1バイトずつ読み込んでみたとしても、それがどんなデータを構成している 1バイトなのかが分からないので、 正しく扱うことができないのです。

また、int型と double型が登場していますが、int型の大きさが異なる環境で作成されたファイルであれば、このサンプルプログラムでは正しく読み込めないでしょう。 同様に、浮動小数点数の表現方法が異なる環境で作成されていたら、やはり読み込めません。 このように、バイナリファイルの読み書きは、その中身のフォーマットが正確に分かっていないと、正しく扱うことができないのです。


fread関数でのエラーチェックは、戻り値が、第3引数の値よりも小さいかどうかで判断できます。ファイルの終わりに達した場合もこれで検出されてしまうので、切り分ける必要があるのなら、更に feof関数を使って判定します。
このサンプルプログラムでは、読み込むファイルの内容について熟知しているので、ファイルの終わりが来ることを考慮していませんが、データ件数が不明な場合などには、チェックが必要でしょう。

if( fread( &num, sizeof(num), 1, fp ) < 1 ){
    if( feof( fp ) ){
        /* ファイルの終わり */
    }
    else{
        /* エラー発生 */
    }
}

ランダムアクセス

前の章で、fseek関数を説明しました。 テキストファイルの場合には色々と制約が多かったですが、バイナリファイルの場合は自由度が高くなっています。

バイナリファイルに対する fseek関数の使用時には、第2引数の移動量のところに自由に値を指定でき、1バイト単位でファイルポジションを移動できます。 ただし、バイナリファイルに対する fseek関数において、第3引数を SEEK_END にしたときに、これがどんな結果になるかは環境依存となります

このような制限があるため、ファイルサイズを調べるためのテクニックとして、よく使われている以下の方法は、環境依存の方法です。

#include <stdio.h>
#include <stdlib.h>

long GetFileSize(FILE* fp);

int main(void)
{
    FILE* fp;

    fp = fopen( "test.bin", "rb" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }
    
    printf( "%ld\n", GetFileSize( fp ) );

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

long GetFileSize(FILE* fp)
{
    long fpos_save, size;

    /* 現在のファイルポジションを保存 */
    fpos_save = ftell( fp );

    /* ファイルの末尾まで移動して、その位置を調べる */
    fseek( fp, 0, SEEK_END );
    size = ftell(fp);

    /* ファイルポジションを元に戻す */
    fseek( fp, fpos_save, SEEK_SET );

    return size;
}

実行結果(標準出力)

19

GetFileSize関数は、fseek関数でファイルの末尾まで移動し、その位置で ftell関数を呼び出すことによって、ファイルの先頭からの距離を調べています。バイナリファイルの場合は、ftell関数が返す値は必ず、ファイルの先頭からのバイト数であることが保証されているので、この値はファイルサイズと一致します。

最後に、元のファイルポジションに戻してやるところまで面倒を見たいので、 あらかじめ、ftell関数で返されるファイルポジションを保存しておき、最後にその位置に戻しています。

改行文字

バイナリファイルには、文字を含むことができるので、テキストファイルとまったく同じデータで構成されていても構いません。 しかし、この章の冒頭で触れたように、改行文字の扱いが異なります。

C言語において、改行文字といえば '\n' をイメージしますが、これはC言語の文法上のルールに過ぎません。 実際のファイル内に '\n' がそのまま書き込まれている訳ではありません。

例えば、Windows環境で、テキストファイルに '\n' を含んだ文字列を出力して、出来上がったファイルをバイナリエディタで覗き見てみましょう。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE* fp;

    fp = fopen( "test.txt", "w" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    /* 改行文字を含んだ文字列を書き込む */
    fputs( "xyz\nxyz", fp );

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

実行結果(標準出力)


実行結果(test.txt)

xyz
xyz

fopen関数の第2引数は "w" なので、テキストファイルとして書き出していることに注意して下さい。 fputs関数に、途中に改行文字を含ませた文字列を渡して、ファイルへ書き出しています。

作成された test.txt を、バイナリエディタで確認すると、次のようになっています。

改行文字をバイナリエディタで確認

アドレス00000000~00000002 と 00000005~00000007 は、いずれも "xyz" ですから、その間にあるのが改行文字だと考えられます。 ところが、ここには 2バイト分のデータ「0D 0A」が存在しています。

このように、Windows環境では、改行は2つの文字で表現されます。 1つは、数値上「0D」で表されるキャリッジリターン(復帰)、 もう1つは、数値上「0A」で表されるラインフィード(改行)です。 前者を CR、後者を LF と略し、あわせて CR+LF のように表記することもあります。

環境によっては、CR と LF のいずれか一方だけで、改行を表すこともあります。 例えば、macOS では、LF だけが使われるので、先ほどのプログラムを macOS 環境の clang でコンパイルして実行してみると、 標準出力に現れる結果は同じに見えますが、バイナリエディタで見ると「0A」だけしか無いことが分かります。

改行文字をバイナリエディタで確認

エンディアンの話と同様、改行も環境に対する依存性があるということです。 複数あり得る改行の表現を、C言語では '\n' という1つの表現方法に統一させることで、環境ごとの違いを吸収しています。 そして、テキストファイルとして扱う場合には、'\n' を出力すると、CR+LF や LF といった、その環境に応じた表現に変換されますバイナリファイルとして扱う場合には、このような変換を行わず、'\n' は「改行」を意味する値のまま、つまり「0A」として出力されます。


このような、改行文字の自動的な変換は、読み込みの場合にも行われます。 読み込みの場合は、CR+LF や LF などの形で表現された現実の改行の表現を検出すると、'\n' に直して読み込みます。
先ほどのサンプルプログラムを実行して出力された test.txt を読み込んで、1文字ずつ出力するプログラムを作って確かめてみましょう。

#include <stdio.h>
#include <stdlib.h>

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


    fp = fopen( "test.txt", "r" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    for( i = 0; ; ++i ){
        c = fgetc( fp );
        if( feof(fp) ){
            break;
        }
        else if( ferror(fp) ){
            fputs( "読み込み中にエラーが発生しました。\n", stderr );
            exit( EXIT_FAILURE );
        }
        else{
            /* 何もしない */
        }
        
        printf( "%d: %c\n", i, c );
    }

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

test.txt:

xyz
xyz

実行結果(標準出力)

0: x
1: y
2: z
3:

4: x
5: y
6: z

出力結果の3文字目(最初を 0 とする)のところで、余分に改行されているようです。 test.txt の3文字目は CR、4文字目に LF です。 Windows環境では、CR+LF で改行文字を表すので、この2文字の組み合わせを '\n' に変換しています。

macOS の場合は、ファイルには LF だけが書き込まれているはずです。 その場合、LF を '\n' に変換します。

このように、ファイルを作った環境で、読み込む環境が同一ならば、ほとんど何も気にする必要がありません。 問題は、Windows で作ったファイルを、macOS で読み込むなどというような場合です。
改行が CR+LF となっているファイルを、改行が LF だと仮定しているプログラムで読み込むと、LF だけを改行と認識することになるでしょう。 恐らく、改行の手前に余分なゴミ(CR) が現れることになります。
反対に、改行が LF となっているファイルを、改行が CR+LF だと仮定しているプログラムで読み込むと、LF 単体では改行と認識しないので、 一切改行が行われず、いたるところにゴミ(LF) が現れることになります。


バイナリファイルの場合を実験してみましょう。 バイナリファイルにとって、改行文字というのは、単なる 1バイトのデータに過ぎず、何も特別扱いはしません。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE* fp;
    char str[] = "xyz\nxyz";


    fp = fopen( "test.bin", "wb" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    fwrite( str, sizeof(str[0]), sizeof(str), fp );

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

実行結果(標準出力)


実行結果(test.bin のテキスト表現)

xyz
xyz

test.bin をバイナリエディタで開くと、次のようになります。

改行文字をバイナリエディタで確認

今度は、改行文字の部分は「0A」という 1バイトだけになっています。 「0A」は改行のことなので、特に変換せず、素直にそのまま出力されている訳です。 この結果は、Windows でも macOS でも同じになります。

ところで、C言語のソースコード上で「改行」は '\n' でしたが、「復帰」の方も '\r' という表現方法を持っています。 テキストファイルを使う場合は、'\r' を使う機会はありませんが、バイナリファイルの場合には使うことがあります。

例えば、改行が CR+LF で表現されているテキストファイルを、改行を LF だと仮定するプログラムで読み込むことを考えます。 この場合は、バイナリファイルとして読み込みを行うようにします。 すると、CR+LF の部分は、変換されることなく '\n' と '\r' として読み込まれます。 この組み合わせを見つけたら、'\n' に手動で変換してやれば良いのです。


練習問題

問題① 手元にある適当なファイルを幾つかバイナリエディタに読み込ませて、中身を確認してみて下さい。

問題② 次のような構造体型があります。

typedef struct NameList_tag {
    size_t nameLength; /* name の文字数 (終端文字を除く) */
    char* name;        /* 名前 */
    int age;           /* 年齢 */
} NameList;

この型で表現された以下のデータを、バイナリ形式でファイルへ出力するプログラムおよび、 ファイルから入力を受け取るプログラムを作成して下さい。

static const NameList nameList = {
    4, "John", 29
};

問題③ リトルエンディアンとビッグエンディアンを相互に変換するためには、どのようにすれば良いか考えて下さい。


解答ページはこちら

参考リンク

更新履歴

'2018/3/17 全面的に文章を見直し、修正を行った。
項の順序を入れ替えて、サンプルプログラムを見る前に、バイナリエディタを説明するようにした。 また、その項の一部を「文字コード」として分離した。

'2018/3/16 「ランダムアクセス」の項から、文章の一部を、第41章へ移動。

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

'2018/2/21 文章中の表記を統一(bit、Byte -> ビット、バイト)

'2015/8/29 flose関数の戻り値もチェックするようにした。

'2014/1/31 OS X 環境に対応。

'2010/5/23 新規作成。





前の章へ(第41章 ランダムアクセス)

次の章へ(第43章 バッファリング)

C言語編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS