ファイルの読み込み

ファイル処理1

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

ファイル操作は「ファイルを開き」「読み書きをして」「ファイルを閉じる」という手順で行います。
ノートに例えるならば、ノートを開き、文章を読み、ノートを閉じる、という手順です。

ファイルを開くにはfopen関数を、ファイルを読むにはfgets関数を、ファイルを閉じるにはfclose関数を使用します。

ファイル処理の項目でのサンプルコードは、操作対象のファイルは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」という名前で保存してください。
その後、このコードを実行してテキストファイルの内容が表示されることを確認してください。

文字化けする場合は、テキストファイルの文字コードを変更してみてください。
Windowsなら「Shift_JIS」、MaxOSやLinux系OSなら「UTF-8」が標準です。
それでも文字化けする場合は「EUC-JP」にしてみてください。

サンプルコードで使用しているファイルのパスの区切り文字(\記号)はWindowsの場合です。
\はC言語ではエスケープシーケンスなので、\記号自体を表すのに\\とする必要があります。

他のOSではスラッシュ記号(/)を使用してください。
実はWindowsでもコード上ではスラッシュ記号を区切り文字に使用することが可能なので、以下のようにしても良いです。


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

スラッシュは特殊文字ではないので、エスケープの必要はありません。
また、関数によっては\記号以外の区切り文字は使用できない場合もあります。

fopen関数

fopen関数は指定のファイルを開きます。

FILE *fopen(
 const char *filename,
 const char *mode
);
ファイル名filenameを、ファイルモードmodeでオープンする。
戻り値はFILE構造体のポインタ。
オープンに失敗した場合は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行目)

FILE型は構造体で実装されていることが多いと思いますが、構造体でなければならないと決められているわけではありません。
内部構造はプログラマが知る必要はなく、構造体であったとしても各メンバにプログラマが直接アクセスすることはありません。

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

テキストファイルは、人間が読んで理解できる言葉が書いてあるファイルです。
メモ帳で作った「.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;
errno_t err = fopen_s(&fp, "test.txt", "r");

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

//errno_t型はただの整数型なので
//intで受け取ることも可能
//int err = fopen_s(&fp, "test.txt", "r");

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

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


FILE *fp;

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

fopen_s関数でファイルのオープンに失敗した場合もFILE構造体の変数がNULLになるので、これで成否の判定をすることもできます。

fgets関数

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

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

まずはファイルから読み取ったデータを保存するためのchar型配列を用意します。
ファイルの全てのデータを一気に読み取るわけではないので、配列のサイズはいくつでも構いません。
ただし小さすぎると処理時間が多くかかり、大きすぎるとメモリの消費量が大きくなります。

fgets関数の第一引数には、上記のchar型配列を指定します。
第二引数には、配列のサイズを指定します。
第三引数には、fopen(fopen_s)関数で得られたFILE型ポインタを指定します。

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

末尾に改行文字が付与される可能性があるため、これをそのまま別の関数等に使用すると問題が発生する可能性があります。
必要に応じて末尾の改行の除去します。


//NULL文字の手前の文字
size_t last = strlen(line) - 1;

//改行文字だったらNULL文字に置き換え
if (line[last] == '\n')
	line[last] = '/0';

ただしマルチバイト文字の場合は上記コードでは上手くいかない可能性があります。

読み込むファイルサイズよりも大きなchar配列を用意すれば、ファイルの全てを一度に読み込むことができます。
しかし実際のプログラムでは読み込みたいファイルサイズが事前に決まっていないことも多く、巨大な配列を用意するのはメモリの無駄です。
なので、そこそこのサイズの配列を用意し、そこに入るだけのデータを読み取ります。
読み取ったデータを画面に表示したら、続きのデータを読み取ります。
これをファイルの終端まで繰り返すことで、全ての内容を取得します。


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

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

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以外を返す。

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

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

ストリームとは

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

絶対パスと相対パス

コンピューター上にあるファイルやフォルダ(ディレクトリ)の場所を示す文字列をパスといいます。
(ファイルパス、フォルダパス(ディレクトリパス)、など)

ファイル構造は階層構造(木構造)になっていて、最上位のフォルダからデータが階層的に枝分かれしていく形になっています。


//木構造の例
root ┬ aa ─ dd ┬ ii
     ├ bb ┬ ee └ jj
     │    └ ff
     └ cc ┬ gg ┬ kk
          └ hh └ ll

例えば上図のような構造の場合、「kk」というデータは「root/cc/gg/kk」という場所にある、と言うことが出来ます。
このような、最上位から順に見た位置情報を絶対パスと言います。

これに対して、あるデータからの相対的な位置の示し方を相対パスと言います。
例えば「kk」を起点として「ll」は、同じ階層にあるデータです。
この場合は単に「ll」あるいは「./ll」で表すことができます。
「./」は同じ階層にあるデータを意味します。

「kk」を起点として「gg」は自分自身(kk)を格納するディレクトリです。
これは「../」で表すことが出来ます。
その上の「cc」は「../../」で表します。
「kk」から見て「hh」は、「二つ上のディレクトリ内にあるデータ」なので「../../hh」で表すことができます。

ちなみに、階層構造の最上位はルートディレクトリ(ルートフォルダ)や単にルートといいます。
WindowsではCドライブやDドライブなどがそれぞれルートフォルダになります。
UNIX系OS(Linux、macOSなど)ではドライブが複数あってもルートとなるディレクトリは「/」のみで、ここから全てのデータが枝分かれしていきます。

なお、Visual Studio上からプログラムを実行している場合はプロジェクトファイルのあるフォルダが相対パスの起点となります。
(実行ファイルやソリュージョンファイルのあるフォルダではない)