第55章 ファイルのススメ4

 ファイルには文字列しか書き込めないわけではありません。およそメモリ上で表現できるデータは全て書き込むことが出来ます。今回はメモリ上の生の数値データをファイルに書き込んでみましょう。


 では、今回の要点です。


 では、いってみましょう。


 今回は前回書き込んだデータを生のデータで読み書きしてみましょう。そのため、今回はバイナリモードを使うことにします。

 前回は、文字列を入力させ、その長さと文字列の文字コードを数字(文字列)で書き込みました。この際、長さは10進数、文字コードは16進数で書き込みました。

 しかし、今回は生のデータを読み書きします。なので、このように進数を考えることはありません。では、次のプログラムを見て下さい。

プログラム
// File3b.cpp
#include <stdio.h>
#include <string.h>

int main()
{
    FILE* pFile;
    char  buffer[512];
    int   nLength;

    printf("何か文字列を入力して下さい > ");
    gets(buffer);

    pFile = fopen("fwrite.txt", "wb");

    nLength = strlen(buffer);
    fwrite(&nLength, 4, 1, pFile);
    fwrite(buffer, 1, nLength + 1, pFile);

    fclose(pFile);

    return 0;
}
実行結果
何か文字列を入力して下さい > 1234567890
fwrite.txt の中身(16進ダンプ)
0A 00 00 00 31 32 33 34 35 36 37 38 39 30 00

 「16進ダンプ」というのは、ファイルの中に書き込まれている数値を、16進数で書き下したものです。別に何進法で書き下してもいいのですが、こういう時は通例16進法を使います。

 それでは、プログラムを見ていきましょう。

 先ずは、文字列の長さを一旦変数に入れておきます。そして、次に fwrite という関数でファイルに書き込んでいます。fwrite はメモリ上にあるデータをそのままの形でファイルに書き込む関数なので、書き込む内容は変数上に置いていなければなりません。そのため、 文字列の長さを一旦変数に入れておきました。ついでに、この情報は次の文字列の保存でも使います。

 では、fwrite の引数を見ていきましょう。

fwrite(&nLength, 4, 1, pFile);

 初めの &nLength は、書き込みたいデータの置いてあるアドレスです。書き込みたいデータは nLength に入っているので、先ずこのアドレスを渡します。

 次の 4 は、書き込むデータの1要素のサイズです。そして、次の 1 は、書き込むデータの要素数です。配列を書き込むときに、配列の1つ1つのサイズと、そして配列の要素数を指定すればいいようになっているわけです。が、別にこれらを無茶苦茶に使っても問題ありません。面倒なら、一方を1にして、もう一方にデータ全体の長さを指定したのでも構わないでしょう。

 で、最後の pFile は言うまでもありませんね。このように指定すれば、ファイルにデータを書き込むことが出来ます。

 で、次には文字列を書き込んでいます。先ず、書き込みたいデータのアドレス buffer を渡し、次に1要素のサイズ 1 を渡します。で、要素数、つまり文字数 nLength + 1(文字列の終わりのヌルも含む)を渡し、最後に pFile を渡しています。それ程問題はありませんね。

 で、最後にファイルを閉じて、終了です。


 と、とりあえずファイルに書き込んでみましたが、いろいろ疑問に思った方もいるでしょう。ちょっとここで、解説を加えます。

出るであろう疑問その1
何故 fprintf の時は、最終的に書き込まれるデータを変数(メモリ)に入れていなくても良かったのか?

 これは、fprintf が内部で処理してくれるからです。内部で、fprintf が書き込むべき文字列をメモリ上に生成し、それを書き込んでくれるようになっています。fwrite はこのような仕様にはなっていません。

出るであろう疑問その2
進数を考えて、もしくは考えずに書き込むっていうところがよく分からない。

 fprintf は、データを文字列として書き込む関数でした。例えば 32 という数値を10進数として書き込むとすると、それは文字列 "32" つまり、文字コードで言うと 0x33, 0x32 という2バイトのデータを書き込むことになります。一方、これを16進数として書き込むとすると、32は16進数で20ですから、それは文字列 "20" つまり 0x32, 0x30 という2バイトのデータを書き込むことになります。

 一方、fwrite はメモリ上にあるものをそのまま書き込むので、進数とかは無関係です。10進数と解釈するのも、16進数と解釈するのも、なんと解釈するのも勝手です。32 という数値を10進数として解釈しようと、60進数として解釈しようと、メモリ上の表現には全く影響を及ぼさないのです。

 考えてみて下さい。今 int 型の変数 a に 64 という数値が入っているとします。これを私たちが「16進数の 40 だ」とか「7進数の 91 だ」とか考えたところで、メモリ上のデータは変わらないでしょう? 物理的にあり得ないことですね。

 これで分かりましたでしょうか?

出るであろう疑問その3
テキストモードで fwrite を使うと、改行はどうなるのか?

 テキストモードなので、改行コードは変換されます

 例えば int a = 13; の a をテキストモードで書き込もうとすると、'\n' は13なので、意図した通りに書き込まれないことになります。注意しましょう。

 その逆で、バイナリモードで改行を書き込むときは、あらかじめ改行コードを変換しておく必要があることも注意して下さい。変換後の改行は、Windowsでは '\r\n' 、Macでは '\r' 、UNIXでは '\n' です。

出るであろう疑問その4
文字列の長さ10が 0x0A000000 として書き込まれているのでは?

 メモリ上で実際にこういう風に値が格納されているということです。

 若いアドレスの方に、下位のバイトが格納されていますね。こういうデータの格納形式を「リトルエンディアン」と言います。語源はガリバー旅行記にあります。

 逆に、大きいアドレスの方に、下位のバイトが格納されるというデータ形式を「ビッグエンディアン」と言います。こういったデータの格納方式のことをエンディアンネスと呼びます。

 この両者の格納形式は、CPUによって違います。Windowsの動く環境ではリトルエンディアン、Macの動く環境ではビッグエンディアンになっていると思います。UNIXはC言語でできることの全てを実現できる環境ではどこでも動くので、エンディアンネスは環境によって変わります。


 まだ疑問に思うことがあるかもしれませんが、一応先に進みます。まだ疑問に思うところがあれば、何度もテストプログラムを組んでみて理解して下さい。


 では、今度は読み出しをやってみましょう。では、プログラムを見て下さい。

プログラム実行結果
// File4b.cpp
#include <stdio.h>

int main()
{
    FILE* pFile;
    int   nLength;
    char  buffer[512];

    pFile = fopen("fwrite.txt", "rb");
    if(pFile == NULL)
      return 0;

    fread(&nLength, 4, 1, pFile);
    fread(buffer, 1, 512, pFile);
    buffer[511] = 0;

    printf("文字列の長さは %d バイトです。\n%s\n", nLength, buffer);

    fclose(pFile);

    return 0;
}
文字列の長さは 10 バイトです。
1234567890

 生のデータでの読み出しには fread という関数を使います。引数は fwrite と同じです。読み出すか、書き込むか、の違いだけです。簡単ですね。

 で、あとはそれを printf で表示しています。ここは何も問題ないですね。

 一応問題としては、2つめの fread で512バイトも読み出そうとしているというのがあるかと思います。このファイルが上のプログラムで作られたのであれば、nLength + 1 にしても正常動作します。ですが、いたずらでここに512以上の値が入れられて、データも512バイト以上用意されているかもしれません。その時は、buffer を越えてデータが読み出されることになります。これは危険なので、上のようにしました。

 こんなテストプログラムではここまでする必要もないかもしれませんが、実用する段階になればこのようなことを考えるのも必要になってきます。もちろん、nLength が512以上なら nLength を511にして...という処理でも構いませんし、このあとにもデータが続くようであれば512バイト固定で読み出すこともできないでしょう。どういう処理を行うかは各自に任せるとしまして、とにかく、ファイルは「別のプログラムでも簡単に操作できる」という危険性を持っているのだ、ということを忘れないようにしましょう。

 あと、ファイルサイズ以上のデータを読み出そうとしても、別にエラーは出ません。fread は読み出したバイト数を返しますので、この数値が読み出そうとしたバイト数よりも小さければファイルの終端に達したことが分かります。

 正確にはファイルの読み出しエラーが生じたという可能性もあります。なので、厳密には feof という関数で確認する必要があります。feof(pFile) が0以外の時、ファイルの終端に達していることになります。if(feof(pFile)) 〜 とできますね。


 今回はこれで終わりです。ちょっと長かったでしょうか? 次回はファイル操作を離れて、ちょっと簡単なことをしてみます。

 では、今回の要点です。


 では、次回まで。


第54章 ファイルのススメ3 | 第56章 身長いくら?

Last update was done on 2000.8.6

この講座の著作権はロベールが保有しています