C言語編 第39章 ファイルの利用

先頭へ戻る

この章の概要

この章の概要です。

ファイル

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

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

ストリーム

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

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

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

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

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

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

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

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


ファイルへの出力

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

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

int main(void)
{
    FILE* fp;

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

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

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

    return 0;
}

実行結果(標準出力)


実行結果(hello.txt)

Hello, World

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

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

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

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

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

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

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

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

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

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

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

fopen関数によってストリームに結びついたファイルは、必要な情報を入れた FILE という型のオブジェクトを使って操作します。FILE の正体は処理系依存であり、その内容を直接触れることはありません。代わりに、FILE型のオブジェクトを指すポインタだけを使います。ファイル操作に関する標準ライブラリ関数は多数ありますが、そのいずれもが、FILE型のポインタを渡すように設計されています。

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

int fclose(FILE* stream);

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

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

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

世の中には、fclose関数の戻り値をチェックしていないプログラムが溢れていますが、必ずチェックするべきです。例えば、ファイルへデータを出力するプログラムでは、その出力時に起こるエラーが、出力のための関数のところではなく、fclose関数のところで検出される可能性があるからです。

これは、出力処理が、出力の関数の中で即座に行われるとは限らず、バッファリング(第43章)されていることがあるためです。この場合、fclose関数でファイルを閉じるときに、バッファの内容をファイルへ出力しようとするので、このタイミングでファイルにアクセスできないなどの問題が起きると、fclose関数のエラーとして返されることになります。例えば、fopen関数、fputs関数を呼び出したときにはアクセスできていた SDカードのようなデバイスが、fclose関数を呼び出すタイミングでは物理的に引き抜かれていてアクセスできない、といったケースがあり得ます。

エラーが発生してしまった場合、ファイルへの出力は行えていない可能性があります。エラーを無視してプログラムの実行を続けてしまうと、出力すべきだった情報を失う恐れがあるので、何らかの対策を講じる必要があります。真面目な対策方法は、そのプログラムの仕様次第です。先ほどのサンプルプログラムでは、エラーメッセージの出力だけ行い、exit関数で強制終了させています。


間を飛ばしてしまいましたが、ファイルへの出力を行っているのが fputs関数です。

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

ファイルからの入力

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

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

int main(void)
{
    FILE* fp;
    char buf[40];

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

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

    return 0;
}

fopen関数の第2引数を "r" にしています。これはファイルから入力する(読み込む)ことを意味しています。このオープンモードであれば、fgets関数を使って、ファイルから入力を受け取ることができます。

hello.txt:

Hello, World

実行結果(標準出力)

Hello, World

実行結果(標準エラー)




ここでは、ファイルがオープンできなかった場合について確認しておきましょう。hello.txt が存在しない状態でこのプログラムを実行すると、fopen関数が失敗してヌルポインタを返します。すると、次のような結果を得られます。

hello.txt:

Hello, World

実行結果(標準出力)


実行結果(標準エラー)

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

ファイルからの入力では、ファイルが存在していないことが理由で失敗するというケースがあるので、きちんと対策を取る必要があります。

ファイル名の指定

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

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

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

相対パスによる表記は、現在注目しているディレクトリ(これを、カレントディレクトリと言います)を起点として、 そこから目的のファイルまでの経路で書き表す方法です。 先ほどの絶対パスの例と同じファイルを相対パスで指定することを考えます。
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" と書けば相対パスとみなされます。 この場合のカレントディレクトリは、特に何もほかの制御の影響を受けていなければ、実行ファイルがあるディレクトリです。

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

なお、ほとんどのケースでは相対パス表記を使うべきです。 絶対パス表記では、プログラムを開発した本人のディレクトリ構造に依存しており、 ほかの 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" が含まれるオープンモードは、追記書き込みを行うモードです。 この場合は、元々あったファイルの中身は失われず、末尾へ付け足すように書き込みを行えます。

テキストファイルバイナリファイルの違いは、改行文字の扱いです。 改行文字に関する詳細な説明は第42章で行いますが、 簡単にいうと、テキストファイルでは、プログラム上での改行の指示と、環境に固有の改行文字との間での変換を行います。 バイナリファイルではこの変換を行いません。
ただし、テキストファイルとバイナリファイルを区別しない環境もあります。 Windows は区別を行う環境なので、きちんと使い分ける必要があります。

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

ファイルを扱う際の注意

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

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


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

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

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

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

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

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


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


練習問題

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

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

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

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


解答ページはこちら

参考リンク



更新履歴

'2018/6/4 「標準エラー」の項を内容の一部を、第30章へ移動。
「ファイルへの入出力」の項を「ファイルへの出力」に、「標準エラー」の項の一部を「ファイルからの入力」に変更。

'2018/5/25 第48章の練習問題③を移動してきて、練習問題④とした。

'2018/4/20 「NULL」よりも「ヌルポインタ」が適切な箇所について、「ヌルポインタ」に修正。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/3/15 全面的に文章を見直し、修正を行った。
fopen関数のオープンモードに関する話題を、「ファイルのオープンモード」の項に分離した。

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



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

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

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

Programming Place Plus のトップページへ


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