ファイルの読み込み

ファイル処理1

今までのプログラムは、画面に何か表示して終わりという単純なものばかりでした。
プログラムの実行結果をファイルとして残したり、設定ファイルとして次回起動時にプログラムに読み込んだりできれば、プログラムの幅は大きく広がります。

ファイル処理の項目でのサンプルコードは、分かりやすいようにCドライブ直下にファイルを作成したり読み込みしたりするようにしています。
しかし環境によってはCドライブ直下にはファイルを作成できない場合があります。
その場合はマイドキュメントやVisual Studioのプロジェクトフォルダ内など、適宜パスを置き換えてください。

#include <stdio.h>

#define BUFFER 256

int main()
{
    const char *file = "C:\\test.txt";

    FILE *fp;

    //fp = fopen(file, "r");
    fopen_s(&fp, file, "r");
    
    if (fp == NULL)
    {
        printf("%sのオープンに失敗しました。\n", file);
        printf("Enterキーで終了。\n");
        getchar();
        return 0;
    }

    char line[BUFFER];
    while(fgets(line, BUFFER, fp) != NULL)
    {
        printf("%s", line);
    }

    fclose(fp);

    getchar();
}

このサンプルコードは、「C:\test.txt」というファイルを読み込み、画面に表示します。
指定のファイルがなければ「C:\test.txtのオープンに失敗しました。」と表示され、プログラムは終了します。

試しにテキストエディタ(メモ帳)に何か適当な文字列を打ち込み、Cドライブ直下に「test.txt」という名前で保存してください。
その後、このコードを実行して先ほど打ち込んだ文字列が表示されることを確認してください。

fopen関数

ファイルからデータを読み込むには、「オープン」「読み込み」「クローズ」という三つの手順を経る必要があります。
ノートに例えるならば、ノートを開き、文章を読み、本を閉じる、という手順です。

ファイルをオープンするにはfopen関数(fopen_s関数)を使用します。

FILE *fopen(
 const char *filename,
 const char *mode
);
filenameを、ファイルモードmodeでオープンする。
戻り値はファイルへのポインタ。
オープンに失敗した場合はNULLポインタを返す。

fopen関数には、通常版とセキュア版があります。
Visual Studio2015ではfopen関数を使用するとエラー(警告)となるため、fopen_s関数を使用します。

まずは通常版であるfopen関数から解説します。

fopen関数の第一引数はファイル名です。
これは絶対パス、または相対パスで指定します。
ディレクトリ構造を示す円記号は文字列中にそのまま書けませんから、エスケープシーケンスを用います。
(C:\test.txt→"C:\\test.txt")

第二引数にはファイルのオープンモードを指定します。
オープンモードは以下の文字列を指定します。
(一文字のものでも「文字列」つまりダブルクォーテーションで括ります)

r
テキストファイル
読み取りモードで開く
ファイルが存在しない場合はエラー
w
テキストファイル
書き込みモードで生成
既にファイルが存在する場合は中身を消去する
a
テキストファイル
追加モードで開く
ファイルの終端から書き込みを行う
ファイルが存在しない場合はファイルを生成
r+
テキストファイル
更新モード
読み取りと書き込みを同時に行う
ファイルが存在しない場合はエラー
w+
テキストファイル
更新モード
読み取りと書き込みを同時に行う
既にファイルが存在する場合は中身を消去する
a+
テキストファイル
追加更新モード
ファイルの終端から書き込みを行う更新モード
ファイルが存在しない場合はファイルを生成
rb
バイナリファイル
読み取りモードで開く
ファイルが存在しない場合はエラー
wb
バイナリファイル
書き込みモードで生成
既にファイルが存在する場合は中身を消去する
ab
バイナリファイル
追加モードで開く
ファイルの終端から書き込みを行う
ファイルが存在しない場合はファイルを生成
rb+
(r+b)
バイナリファイル
更新モード
読み取りと書き込みを同時に行う
ファイルが存在しない場合はエラー
wb+
(w+b)
バイナリファイル
更新モード
読み取りと書き込みを同時に行う
既にファイルが存在する場合は中身を消去する
ab+
(a+b)
バイナリファイル
追加更新モード
ファイルの終端から書き込みを行う更新モード
ファイルが存在しない場合はファイルを生成

種類が多く見えますが、以下のように分類できます。

  • 対象ファイル形式
    • テキストファイル
    • バイナリファイル
  • オープンモード
    • 読み取り
    • 書き込み
    • 更新(読み書き)
    • 追加

さて、ファイルが無事オープンできた場合、fopen関数はファイルへのポインタを返します。
このポインタのデータ型はFILE構造体という特殊なデータ型です。
7行目で宣言したFILE構造体のポインタ変数fpで、fopen関数の戻り値を受け取ります。
ファイルの読み書きは、このFILE構造体のポインタ変数を通して行われます。

ファイルオープンに失敗した場合、fopen関数はNULLを返します。
ポインタ変数fpがNULLならばエラーメッセージを画面に表示し、プログラムを終了します。
(13~18行目)

テキストファイルとバイナリファイル

テキストファイルは、人間が読んで理解できる言葉が書いてあるファイルです。
メモ帳で作った「.txt」ファイルや、「.html」「.ini」ファイルなどがテキスト形式のファイルです。
プログラムのソースコード(.cや.cppファイル)などもテキストファイルです。

バイナリファイルは、コンピューターが理解できる形式のファイルです。
画像ファイルや音楽ファイル、プログラムの実行ファイルなど、テキスト形式のファイル以外はすべてバイナリファイルです。
バイナリファイルをテキストエディタで無理やり開いてみても普通の人には理解できません。

fopen_s関数

errno_t fopen_s(
 FILE** pFile,
 const char *filename,
 *mode
);
filenameを、ファイルモードmodeでオープンし、ファイルへのポインタをpFileに受け取る。
(fopenのセキュア版)
戻り値はエラーコード。
オープンに成功すれば0を、失敗すれば0以外を返す。

fopen_s関数は、戻り値がエラーの判定用の値を返す仕様になっています。
そのため、FILE構造体のポインタは引数で受け取るように変更されています。

第一引数には、ファイル構造体のポインタ変数へのポインタを指定します。
「ポインタのポインタ」という、特殊な形になっていますが、ポインタ変数にアドレス演算子を付けて指定する、と覚えておけば問題ありません。

第二、第三引数はfopen関数と同じです。

fopen_s関数が返す戻り値でエラー判定をしたい場合は、以下のようにします。

FILE *fp;
int err = fopen_s(&fp, "test.txt", "r");

if (err != 0)
{
    //エラー処理
}

fopen_s関数が成功すると、戻り値に0を返します。
0以外が返ってきた場合はオープン失敗なので、エラー処理を行います。

if文の条件判定にすべてまとめることもできます。

FILE *fp;

if (fopen_s(&fp, "test.txt", "r") != 0)
{
    //エラー処理
}

fgets関数

ファイルからデータを読み取るにはいくつか方法がありますが、ここではfgets関数を使用します。

char *fgets(
 char *str,
 int n,
 FILE *stream
);
ファイルストリームstreamからstrにn - 1文字読み取る。
もしくは改行まで読み取る。
戻り値はstr自身。
ファイルの終端に達した場合はNULLを返す。

まずはフィルから読み取ったデータを保存するためのchar型配列を用意します。
(21行目)
配列のサイズはいくつでも構いませんが、小さすぎると処理時間が多くかかり、大きすぎるとメモリの消費量が大きくなります。
fgets関数の第一引数には、このchar型配列を指定します。

第二引数には、第一引数の配列のサイズを指定します。
第三引数には、fopen(fopen_s)関数で得られたファイルポインタを指定します。

fgets関数で読み取れるのは、第二引数に指定した値からマイナス1文字目までです。
マイナス1なのは、最後にNULL文字が入るためです。
途中に改行文字がある場合は、改行文字までの文字列が読み取られます。

読み込むファイルサイズよりも大きなchar配列を用意すれば、ファイルの全てを一度に読み込むことができます。
しかし実際のプログラムでは読み込みたいファイルサイズが事前に決まっていないことも多く、巨大な配列を用意するのはメモリの無駄です。
なので、そこそこのサイズの配列を用意し、その配列を使いまわします。
そのための処理が22~26行目のループ文です。

ファイルの「現在の位置」

FILE構造体は、現在ファイルを読み書きしている位置の情報を持っています。
(ファイル位置表示子ファイル位置インジケーターという)
テキストエディタで例えれば、キャレットの位置です。
(キャレット=多くのテキストエディタで、次に文字が入力される位置を示す点滅する縦棒)
キャレット

fopen関数でテキストファイルを読み取りモードで開くと、ファイル位置は先頭を示した状態になります。
fgets関数でファイルを読み込むと、ファイル位置は読み取られたデータの終端まで移動します。

次にfgets関数を実行すると、以前に読み取った位置から読み取りが再開されます。

fgets関数の繰り返し
※この図はあくまでも動作イメージであり、マルチバイト文字は考慮していません。
また、char配列の最後にはNULL文字が入ります。

このような読み取り処理を繰り返して、ファイルの終端まですべてのデータを読み取るのです。

ファイルの終端に達すると、fgets関数はNULLを返します。
つまり、戻り値がNULLになるまでループを繰り返せば、ファイルの先頭から終端までのすべてのデータを読み込みが終わったことになります。

標準入力から文字列を受け取る

fgets関数は、第三引数にstdinを指定することで、標準入力(コンソール)から文字列を受け取ることもできます。

#include <stdio.h>

int main()
{
	char buf[128];

	printf("何か文字を入力してください。\n");
	fgets(buf, 128, stdin);

	printf("今入力した文字:%s", buf);

	getchar();
}
何か文字を入力してください。
あいうえお
今入力した文字:あいういえお

最後の改行文字も含めて読み取り、配列に格納します。

標準入力から文字を受け取るにはscanf関数がありますが、これは扱いが難しいのでfgets関数を使用したほうが良いです。
scanf関数に関してはscanf関数で詳しく説明します。

fclose関数

オープンしたファイルは、処理が終わったらクローズする必要があります。
それにはfclose関数を使用します。

int fclose(
 *stream
);
ファイルストリームstreamをクローズする。
成功した場合、戻り値は0を返す。
失敗した場合は0以外を返す。

この関数は特に難しいことはありません。
開いた本を閉じる、という処理をしているだけです。

クローズ処理を忘れてしまうとファイルが開きっぱなしとなり、他のプログラムからアクセスできないなどの不具合が起こることもあります。
「開いたら閉じる」ことを忘れないようにしましょう。

ストリームとは

キーボード入力やファイルなどからデータを受け取る際の、データの「流れ」の概念をストリームと呼びます。
受け取りだけでなくデータ出力もストリームに出力し、画面に文字を表示したりファイルに保存したりします。