ファイルの利用 | Programming Place Plus C言語編 第39章

トップページC言語編

このページの概要

以下は目次です。


ファイル

ここまでの章では、データは標準入力から受け取り、標準出力へ出力するという形式のプログラムだけを扱ってきました。この章からは、新たな入出力の方法として、ファイル (file) を使ったものを取り上げていきます。つまり、C言語のプログラムから、ファイルの中身を読み込み(入力)、ファイルへ書き出す(出力)ことをしてみようということです。

ファイルに対する入出力を理解すれば、住所録や名簿のように、プログラムを実行するたびに消えてしまっては困るような場合にも対応できます。ゲームであれば、プレイ記録やハイスコアを残しておくような用途に使えます。実用的なアプリケーションでも、ユーザーの設定情報を保存しておくような用途があります。

また、出力されたファイルを、別のコンピュータ上で実行するプログラムで読み込むこともできます。

このように、ファイルの入出力ができるようになれば、プログラムの幅が大きく広がります。

ストリーム

入出力に関係して、重要な概念にストリーム (stream) というものがあります。ストリームという英単語には「流れ」という意味がありますが、ここではデータの流れる経路のことを意味していると考えます。

プログラムは、データの入力元や出力先を、ストリームという経路で結びつけています。入力データの発生源がキーボードであろうと、ファイルであろうと、プログラムはいつもストリームだけを相手にします。出力も同様で、データの本当の出力先が画面であろうと、ファイルであろうと、ストリームだけを相手にします。

このように、ストリームという概念を1つ間に挟むことによって、実際に入出力を行っているものが何であれ、同じようにプログラミングできます。

入力の仕組みが標準で使うストリームが標準入力、出力の仕組みが標準で使うストリームが標準出力です。標準エラーというものもありました(第30章)。

標準入力は stdin、標準出力は stdout、標準エラーは stderrで表現されます。

ファイルへの入出力を行いたければ、ストリームの指定を変更すればよいのです。たとえば、fgets関数の第3引数に、あるファイルへ結びついたストリームを指定すれば、そのファイルから1行分のデータを入力してくるようになるという訳です。

なお、詳しいことは割愛しますが、ストリームに結び付いている先を変更することが可能です。たとえば、stderr は多くの環境では画面に結びついていますが、これを任意のファイルに結び付け直すことができます。標準出力と標準エラーを、きちんと用途に応じて使い分けていれば、ユーザーの目に見えるべき出力情報は、標準出力によって画面へ表示し、開発者だけが必要とする情報は、標準エラーを通してファイルへ出力するということができます。

ストリームの結び付けを変更するには、freopen関数を使います。


ファイルへの出力

ファイルを扱う最初の例として、ファイルに “Hello, World” と書き込むプログラムを作ってみましょう。

本格的なプログラムでは、色々と考慮しなければならない問題がありますが、まずはすべてがうまくいく前提で書いてみます。

#include <stdio.h>

int main(void)
{
    FILE* fp = fopen("hello.txt", "w");

    fputs("Hello, World\n", fp);

    fclose(fp);
}

実行結果(標準出力)

実行結果(hello.txt)

Hello, World

このプログラムを実行すると、プログラムの実行ファイルがあるディレクトリに、hello.txt というファイルが作られ、そのファイル内に “Hello, World” という文字列が書き出されます。標準出力には何も出力しません。

Windows や macOS の場合、ディレクトリのことをフォルダと呼びますが、意味合いとしては同じものです。

Visual Studio から実行した場合、プログラムの実行ファイルがあるディレクトリではなく、プロジェクトのルートディレクトリ(一番上位のディレクトリ)にファイルが作られます。

ファイルの入出力を行うには、まずストリームをファイルと結びつけなければならないのでした。それを行うのが fopen関数です。fopen関数は、<stdio.h> で次のように宣言されています。

FILE* fopen(const char* restrict filename, const char* restrict mode);

仮引数についている「restrict」については、動作に影響はありませんので、当面無視しておいて結構です。詳細は、第57章で取り上げます。

第1引数にファイルの名前を指定し、第2引数にファイルのオープンモードを指定します。戻り値は、正しく処理が成功した場合には、FILE型のポインタが返され、失敗するとヌルポインタが返されます。

ファイルの名前をどう指定すればよいかという点については、いろいろと説明しなければならないことがあるので、後ほど取り上げます。とりあえず、“hello.txt” のように、Windows のエクスプローラーや、macOS のターミナルなどで表示されるファイル名を指定すればよいです。

Windows のエクスプローラーの場合、デフォルトの設定では、拡張子が表示されないようになっていると思いますが、見えないだけであって、ほとんどのファイルには拡張子があるはずです(拡張子を表示する方法はこちらのページにあります)。拡張子があるのなら、それも含めて指定しなければなりません。

ファイルのオープンモードについても、説明する量が多いので、後ほど取り上げます。先ほどのサンプルプログラムでは、“w” を指定していますが、これはファイルへ出力する(書き出す: Write)ことを意味しています。

ファイルに対して入出力を行える状態にする行為を、ファイルを開く(オープン) (open) と表現します。入出力を終えてストリームから切り離す行為は、ファイルを閉じる(クローズ) (close) といいます。

fopen関数の名前の由来は、「File を OPEN する」ということです。指定したファイルが開かれ、ストリームに結び付けられます。

fopen関数によってストリームに結びついたファイルは、必要な情報を入れた FILE という型のオブジェクトを使って操作します。

FILE の正体は処理系定義であり、その内容に直接アクセスしてはいけません。代わりに、FILE型のオブジェクト(以下、FILEオブジェクト)を指すポインタを、標準ライブラリ関数に渡すというかたちで使用します。

サンプルプログラムでは、fputs関数に FILEオブジェクトを指すポインタを渡しています。fputs関数は、ファイルへの出力を行う標準ライブラリ関数です。fputs関数は、<stdio.h> で次のように宣言されています。

int fputs(const char* restrict s, FILE* restrict stream);

第1引数に、出力する文字列を指定します。puts関数と似た関数ですが、puts関数と違って、自動的に改行文字を付加しません。

第2引数に、FILEオブジェクトへのポインタを指定します。このファイルに対して出力を行いたいという指定になります。

戻り値は、成功したときは 0以上の値、失敗したときは EOF です。EOF はオブジェクト形式マクロで、何らかの負の整数です。

fputs関数を使ってファイルへの出力が行えるのは、fopen関数の第2引数に、“w” のような書き込みを行うオープンモードを指定している場合に限られます。書き込みできないオープンモードを指定していた場合、fputs関数は失敗します。

開かれたファイルは、用が済んだら閉じる必要があります。ちょうど、malloc関数を使ったら、最後に free関数を呼ぶような関係性で、fopen関数を使ったら、最後に fclose関数で閉じます。fclose関数は、<stdio.h> に以下のように宣言されています。

int fclose(FILE* stream);

引数は、fopen関数が返した FILEオブジェクトへのポインタです。free関数と違って、ヌルポインタを渡したときの動作は未定義なので、避けましょう。

戻り値は、成功した場合は 0 を返し、失敗した場合は EOF です。

ファイルが閉じられると、結び付けていたストリームも無効化され、FILEオブジェクトへのポインタも無効になります。そのため、fclose関数を呼び出した後は、渡した FILEオブジェクトへのポインタはもう使ってはなりません。これも free関数に渡したポインタを使えなくなることと同じです。

ファイルからの入力

今度は、ファイルからの入力を試してみます。

#include <stdio.h>

int main(void)
{
    FILE* fp = fopen("hello.txt", "r");

    char buf[40];
    fgets(buf, sizeof(buf), fp);
    puts(buf);

    fclose(fp);
}

hello.txt:

Hello, World

実行結果(標準出力)

Hello, World

実行結果(標準エラー)

fopen関数の第2引数を “r” にしています。これはファイルから入力する(読み込む: Read)ことを意味しています。

読み込みには fgets関数を使っています。これまで標準入力から入力を受け付けるときに使っていた関数ですが、第3引数に FILEオブジェクトへのポインタを渡せば、ファイルからの入力に使うことができます。

エラーの確認

ここまではすべてがうまくいく前提のプログラムでした。実際には、ファイル処理ではいたるところで、処理が失敗してしまう可能性があります。

代表的なケースは、指定したファイルが存在しておらず、オープンに失敗してしまうことです。fopen関数の第1引数の指定ミスの可能性もありますし、そこにあるはずのファイルをユーザーが削除してしまった可能性も考えられます。

ファイル処理に関する標準ライブラリ関数を呼び出すときにはいつも、エラーが発生していないかどうかを確認するべきです。

fopen関数、fputs関数、fgets関数、fclose関数はいずれも、何らかのエラーが発生すると、戻り値によってそれを伝えます。ですから、戻り値を無視せずに確認すれば、エラーチェックを行えます。エラー発生時の戻り値は、fopen関数と fgets関数はヌルポインタ、fputs関数と fclose関数は EOF です。

このうち、fgets関数については少し問題もあります。これは後で取り上げます。

まず、ファイルへ “Hello, World” を書き出す最初のサンプルプログラムに、エラーの確認コードを入れてみます。

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

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

    if (fputs("Hello, World\n", fp) == EOF) {
        fputs("ファイルへの書き込みに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

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

実行結果(標準出力)

実行結果(hello.txt)

Hello, World

fopen関数は、指定されたファイルがオープンできないとエラーになります。エラーの原因となる状況はオープンモードの指定によって異なります。たとえば、指定されたファイルが存在しないケースは、“r” ではエラーですが、“w” ではエラーになりません。“w” は「なければつくる」という仕様だからです。“w” で起こるエラーの原因としては、そのファイルがすでにほかのところで開かれている場合があります。“w” は書き込み目的ですから、書き換えあってしまうことを防ぐためです。

オープン時のエラーの原因はほかにもいくつもあり得ます。環境による違いもあるので、一概にはいえませんが、いずれにしても、エラーチェックは必ず行ってください。

fputs関数は、ファイルへアクセスできないだとか、ファイルが巨大になりすぎたといった理由でエラーになります。

また、ファイルへの出力処理時に起こるエラーの報告は、fputs関数のような出力関数の戻り値ではなく、fclose関数の戻り値で返されることがあります

これは、出力処理が、出力の関数の中で即座に行われるとは限らず、バッファリング (buffering) されていることがあるためです。詳細は(第43章)で説明しますが、ともかく、fclose関数のエラーチェックは重要です

【上級】バッファリングされている場合、fputs関数の呼び出し時には出力を行なわずにバッファに貯めておき、fclose関数でファイルを閉じるときに、一気にファイルへ出力するかもしれません。このタイミングでファイルにアクセスできないなどの問題が起こると、fclose関数のエラーとして返されます。たとえば、SDカードのような抜き差しできるデバイスに保存されているファイルへ書き出そうとしている場合に、fputs関数を呼び出したタイミングではアクセスできていたが、fclose関数を呼び出すタイミングでは引き抜かれていてアクセスできないといったことがあり得ます。

ファイルへの出力を行っているプログラムでエラーを無視してプログラムの実行を続けてしまうと、出力すべきだった情報を失う恐れがあるので、なんらかの対策を講じる必要があります。対策方法は、そのプログラムの仕様次第です。さきほどのサンプルプログラムでは、エラーメッセージの出力だけ行い、exit関数で強制終了させています。必ずしもプログラムを終了させなければならないわけではありませんので、プログラムの仕様をよく検討して決めてください。

ここで、fputs関数でエラーが起きた場合の処置について検討すべき問題があります。このサンプルプログラムでは、エラーメッセージを出力したあと、fclose関数を呼ばずにプログラムを終えています。これは問題ないのでしょうか?

exit関数の仕様で、ファイルを閉じる処理がきちんと行われることが保証されているので、これは問題はありません。ただ、fopen関数と fclose関数の対応関係が崩れているようにみえるので、気持ちが悪いと感じるかもしれませんし、分かりづらさもあるかもしれません。exit関数の仕様を把握していないと、不安でもあります。もちろん、明示的に fclose関数を呼び出してから終了させるという手段をとっても構いませんが、エラーチェックを行う箇所が増えるにつれて辛くなります。

プログラムの終了に _Exit関数abort関数を使うと、ファイルが閉じられる保証はありません(いずれも処理系定義です)。

【C11】quick_exit関数の場合はファイルは閉じられずに終了します。quick_exit関数は、プログラムの終了後に OS などのシステムが後始末をすることを期待しています。

【上級】バッファリング(第43章)されていた場合、exit関数はその内容を出力したうえで、ファイルを閉じます。

ファイルから読み込むほうのサンプルプログラムも同様に、エラーチェックを入れます。

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

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

    char buf[40];
    if (fgets(buf, sizeof(buf), fp) == NULL) {
        fputs("ファイルからの読み込みに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
    puts(buf);

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

hello.txt が存在しない状態でこのプログラムを実行すると、fopen関数が失敗してヌルポインタを返します。すると、次のような結果を得られます。

hello.txt:

Hello, World

実行結果(標準出力)

実行結果(標準エラー)

ファイルオープンに失敗しました。

fgets関数のエラーチェックについては、少し問題があります。

fgets関数がヌルポインタを返す理由の中には、「ファイルの終わりまで到達していて、読み取る文字列がない場合」も含まれています。そのため、fgets関数を繰り返し呼び出すことによって、ファイルの終わりまでに含まれるすべてのデータを読み込むようなプログラムを書くときには、本当にエラーなのかどうかを区別しなければなりません。

この問題については、第40章であらためて取り上げることにします。


ファイル名の指定

ファイルがコンピュータ内のどの位置にあるのかを表現するためには、ファイルパス (file path)(あるいは単にパス (path))の表記方法を理解しなくてはなりません。

ファイルパスの表現には大きく分けて2つの方法があります。1つは絶対パス (absolute path)、もう1つは相対パス (relative path) です。

絶対パスによる表記は、一番上位にあたるディレクトリからたどり、目的のファイルに行き着くまでの経路をすべて綿密に書き表す方法です。たとえば、Windows であれば「C:\Program\test\test.c」、macOS であれば「/Users/myname/Desktop/test/test.c」のような表記になります。なお、一番上位にあたるディレクトリのことを、ルートディレクトリ]{.w} (root directory) と呼びます。

相対パスによる表記は、現在注目しているディレクトリ(これを、カレントディレクトリ (current directory) といいます)を起点として、そこから目的のファイルまでの経路で書き表す方法です。先ほどの絶対パスの例と同じファイルを相対パスで指定することを考えます。

Windows で、カレントディレクトリが「C:\Program」だとすれば、「test\test.c」という表記に、macOS の例では、カレントディレクトリが「/Users/myname/Desktop」だとすれば、「test/test.c」になります。

相対パス表記では、カレントディレクトリよりも上位のディレクトリにあるファイルを指定したいことがあります。このような場合には、「..」という特別な文字列を用いて、1つ上のディレクトリを表現します

たとえば、カレントディレクトリが「C:\Program\test\bin」で、目的のファイルが「C:\Program\test\test.c」のときには、「..\test.c」と記述できます。

また、「.」とすると、カレントディレクトリを意味します

fopen関数の第1引数に指定するファイル名を、どのような表現であるか決めるのは実行環境の問題です。しかし大抵の環境では、絶対パス表記で書けば絶対パスですし、それ以外なら相対パスをみなすでしょう。

たとえば、“test.c” と書けば相対パスとみなされます。この場合のカレントディレクトリは、特に何もほかの制御の影響を受けていなければ、実行ファイルがあるディレクトリです。

Visual Studio から実行した場合は、プロジェクトファイルのある場所がカレントディレクトリとみなされるかもしれません。

なお、ほとんどのケースでは相対パス表記を使うべきです。絶対パス表記では、プログラムを開発した本人のディレクトリ構造に依存しており、ほかの PC では動作しないプログラムになってしまう可能性があります。

【上級】また、これは相対パス表記でも同様ではありますが、セキュリティやプライバシーの面でも注意が必要です。たとえば、個人の名前を使ったディレクトリ名や、コンピュータの内容物の配置を想像できるかもしれないディレクトリ名に注意しないといけません。

Windows と macOS で、ディレクトリ名を区切っている部分の文字が異なっていることに注意してください。基本的には、Windows では「\」が、macOS では「/」が使われます。プログラム内で、ディレクトリ名の区切りが必要な場合は、実行環境に応じて使い分けなければなりません。ただし、Windows でも「/」が使えることが多いので、今後のサンプルプログラムでは「/」で統一します。

「\」を使う場合には、C言語の文字列表現のルール上「\\」のように重ねて表記しないと、エスケープ文字として扱われてしまうことに注意が必要です。これが非常に面倒で忘れやすく、読みづらいため、「/」を使いたいところです。

また、ファイルパスは非常に長くなる可能性があります。fopen関数としては、FILENAME_MAX というマクロで定義された長さまでは扱えることになっています。FILENAME_MAX が表す長さには、文字列の末尾に付けなければならない ‘\0’ も含まれていますから、そのまま char型配列の要素数として使えます。

ファイルのオープンモード

fopen関数の第2引数には、ファイルのオープンモードを指定します。オープンモードには、以下の種類があります。

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

これらの名前の部分を、文字列で指定します。なお、“r” は “read”(読み込み)、“w” は “write”(書き込み)、“a” は “append”(追加)、“b” は “binary”(バイナリ、2進)を意味しています。

“r” が含まれているオープンモードは、指定したファイルが存在しない場合はエラーですが、“w” が含まれているオープンモードでは、自動的に空のファイルが作成されます。

また、“w” が含まれているオープンモードは、ファイルを開いたときに、ファイルの中身が失われます。つまり、まっさらなファイルになり、そこへ書き込み操作を行うことになります。これを望まない場合は、“a” が含まれるオープンモードを使います。

“a” が含まれるオープンモードは、追記書き込み (append) を行うモードです。この場合は、もともとあったファイルの中身は失われず、末尾へ付け足すように書き込みを行えます。

テキストファイル (text file) とバイナリファイル (binary file) の違いは、文字だけで構成されているのかどうかです。

テキストファイルとバイナリファイルの実用上の違いという意味では、改行文字の扱いがポイントになります。改行文字に関する詳細な説明は第42章で行いますが、簡単にいうと、テキストファイルでは、プログラム上での改行の指示と、環境に固有の改行文字との間での変換を行います。バイナリファイルではこの変換を行いません。

ただし、テキストファイルとバイナリファイルを区別しない環境もあります。Windows は区別を行う環境なので、きちんと使い分ける必要があります。

本章では、“r” と “w” しか使いませんでした。“+” が付いたオープンモードについては第40章、“a” が付いたオープンモードについては第41章、“b” が付いたオープンモードについては第42章で取り上げます。

ファイルを扱う際の注意

ざっとファイルに関する基礎知識を学んできましたが、恐らく「聞いたことがあるような言葉は多いけれど、何だか難しそう」というのが印象ではないでしょうか。実際のところ、単にファイルの読み書きすることだけに注力すれば、そんなに難しくはないのですが、とにかくいろいろとややこしいのは確かです。

次章以降、fopen関数の他のオープンモードについて説明していきますが、微妙な違いの多さに戸惑うかもしれません。ファイルの読み書きは、今後もC言語のプログラムを作成し続けていけば、つねに必要になる処理なので、使い続ければ、少しずつでも理解は進むと思います。ですから、学習段階では、あまり細かいところに悩み過ぎない方が健全ではないかと思います。

本章の最後として、ファイルを扱う際の注意事項を少し挙げておきます。

まず、ファイルは複数のプログラムの共有資源かもしれません。たとえば、「メモ帳」のようなプログラムで開いているテキストファイルに対して、自分のプログラムから書き込み操作を行ったら、「メモ帳」の側からすると、突然ファイルの中身が書き変わることになるでしょう。これを防ぐため、同じファイルへの書き込みのオープンは、同時に行えないように制御されていることがあります

あるプログラムで書き込み操作を行い、他のプログラムで読み込みを行うと、読み込むタイミングによってファイルの内容が異なって見えるかもしれません

指定されたファイルパスは、すでに削除されていたり、名前を変更されていたりして、存在しない可能性もあります。

ファイルへのアクセス権限がなく、読み取りや書き込みが行えないかもしれません。

読み込もうとしたファイルの中身が、何者かの手によって、プログラム外で編集されているかもしれません。期待したフォーマット(文法)になっておらず、正しく読み込めない可能性があります。

こういったことは、特に、自分以外の人に使ってもらうプログラムを作る際には、考慮しておかなければなりません。しかしながら、これからファイルの処理を学ぼうという段階で、ここまで考慮することはあまりにも大変です。こういった難しさがあるのだということを頭の片隅に置きつつ、まずは基礎固めをしていきましょう。


練習問題

問題① fopen関数の第1引数を、絶対パスで指定して実行結果を確かめてみてください。

問題② 標準入力からファイル名を入力させ、そのファイルに対して “Hello, World” を出力するプログラムを作成してください。

問題③ 絶対パスと相対パスの使い分けは、実用上どんな違いを生むか考えてみてください。

問題④ ファイルパスを文字列で与えると、拡張子部分を返す関数を作成してください。


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



前の章へ(第38章 ポインタ⑧(関数ポインタ))

次の章へ (第40章 テキストファイルの読み書き)

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る