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

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

ファイル

これまでの章では、データは標準入力から受け取り、標準出力へ出力するという形式だけを扱ってきました。 標準入力や標準出力という言葉に「標準」と付いているように、標準でない入出力の方法も存在しています。 ただし、標準でないからといって、一般的でないという意味ではなく、デフォルトではないという程度の意味合いです。

この章から扱う新たな入出力の方法は、ファイルを使ったものです。 Windows や OS X には、ファイルやフォルダという概念が存在していますが、ここでのファイルもそれと同じものです。 つまり、C言語のプログラムから、ファイルの中身を読み込み(入力)、ファイルへ書き出す(出力)ことができるという訳です。

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

ストリーム

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

プログラムは、データの入力元や出力先を、ストリームという経路で結びつけています。 入力元がキーボードであろうと、ファイルであろうと、プログラムから見えているのは「経路(ストリーム)」だけです。 出力も同様に、出力先がコンソールの画面であろうと、ファイルであろうと関係なく、ストリームだけが見えます。

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

そして、入力と出力のストリームがデフォルトで結びつけているものが、標準入力(キーボード)や標準出力(画面)である訳です。 標準でないファイルへの入出力が必要であれば、そのときどきでストリームによって結び付ける必要があります。

ファイルへの入出力

それでは実際に試してみましょう。

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

int main(void)
{
	FILE* fp;

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

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

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

	return 0;
}

実行結果(標準出力)


実行結果(hello.txt)

Hello, World

このプログラムは、第2章の Hello, Worldプログラムのファイル版です。 標準出力には何も出力されませんが、代わりに hello.txt というファイルに出力しています。

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

Windows や OS X の場合、ディレクトリではなくフォルダと呼ぶことがほとんどですが、意味合いとしては同じものです。

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

ファイルの入出力を行うには、まずストリームによってファイルと結びつけなければならないのでした。 その役割を行うのが、fopen関数(⇒リファレンス)です。
この関数の第1引数にファイルの名前を指定し、第2引数にファイルオープンモードを指定します。 戻り値には、正しく処理が成功すれば、有効な FILE型のポインタが返され、失敗すると NULL が返されます。
この先登場する他の関数も同様ですが、これらは stdio.h に宣言されています。

ファイルの名前には、ディレクトリ名も含めて指定することもできます。 この場合、ファイルパスの表現方法を理解しておく必要があるので、これは後ほど取り上げます
また、拡張子の存在を忘れないようにして下さい。 Windows のエクスプローラの場合、デフォルトの設定では、拡張子が表示されないようになっていると思いますが、見えないだけで確かに存在しています。 拡張子を付けないなら付けないで構わないのですが、例えば、テキスト形式のファイルを出力したとしても、 そのファイルをダブルクリックしても、メモ帳などのアプリケーションで手軽に開けなくなってしまいます。

第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進) を意味しています。

テキストファイルとバイナリファイルの違いは、改行文字の扱いです。 テキストファイルでは、改行文字は、その場所で改行するという動作を指定しますが、バイナリファイルでは、改行文字も単なる文字として扱われます。 ただし、両者の区別の存在しない環境もあります( Windows では区別する必要があります)。

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

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

本章では、とりあえずテキストファイルとしての書き込みの実験だけしかしませんので、他のモードの解説は次章以降に回します。

fopen関数によって、指定したファイルが開かれ、ストリームによって結び付けられます。 そして、その後の操作のために必要な情報を保持した、 FILEオブジェクト(FILE型の変数)(→リファレンス)へのポインタが返されます。 FILE の正体は環境によって異なり、直接、その内容に触れる必要はありません。

開かれたファイルは、用が済んだら閉じる必要があります。 この操作を行うのが fclose関数(⇒リファレンス)です。 ファイルが閉じられると、結び付けていたストリームも無効化され、FILEオブジェクトへのポインタも無効になります。
また、fclose関数は、成功した場合は 0 を返し、失敗した場合は EOF(⇒リファレンス)を返します。 EOF はオブジェクト形式マクロで、これは何らかの負の整数です。

例えば、ファイルへデータを出力するプログラムでは、その出力処理のエラーが、出力用の関数(fputs関数など)のところではなくて、 fclose関数のところで検出される可能性があるので、fclose関数の戻り値もチェックするべきです。 これは、出力処理が即座に行われるとは限らず、バッファリング(第43章)されていることがあるためです。 この場合、fclose関数でファイルを閉じるときに、バッファの内容をファイルへ出力しようとするので、 このタイミングでファイルにアクセスできないなどの問題が起きると、fclose関数のエラーとして返されることになります。


間を飛ばしましたが、ファイルへの出力を行っているのが fputs関数(⇒リファレンス)です。 標準出力へ文字列を出力するとき、puts関数(⇒リファレンス)を使いましたが、 基本的には、それのファイル版です。
基本的にはと書きましたが、決定的な違いが1つあり、それは fputs関数は、puts関数と違って、改行文字を自動的には付けないという点です。 そのため、改行が必要ならば、自分で "\n" を付けて下さい。

fputs関数の第2引数には、FILEオブジェクトへのポインタを渡しています。 これによって、出力先を指定している訳ですが、実はここに指定する記述を変えると、標準出力への出力を行うこともできます。 fputs関数で、標準出力へ出力させるには、次のように記述します。

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

stdout(⇒リファレンス) は、標準出力を意味しています。 これまでにも、標準入力からの入力を受け付ける際に fgets関数(⇒リファレンス)の引数に、 stdin(⇒リファレンス)を指定してきましたが、その出力版です。

標準エラー

標準で用意されているストリームは、標準入力と標準出力だけではありません。 もう1つ、標準エラーというものがあります。
これら3つを合わせて、標準ストリームと呼びます。

標準エラーは、何らかのエラーや問題が発生した際に、その情報を出力するために使われるストリームです。 標準エラーは、標準出力と同様に画面に結び付けられていることが多いです。

標準エラーは、stderr(⇒リファレンス)で表現されます。 これを使って、fputs関数で出力を行うには、

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

stderr の正体は、FILEオブジェクトへのポインタなので、 このように、stderr を指定することができます。

エラーに関するメッセージは、stderr に出力する方が確実です。 ですから、前に挙げたサンプルプログラムにおいて、fopen関数が失敗したときの出力メッセージも stderr へ出力した方が良いでしょう。

#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

ストリームが結び付けている先を変更することは可能です。 そのため、標準出力と標準エラーを用途に応じて使い分けていれば、一方だけ出力先を切り替えることができます。
これによって、ユーザの目に見える出力情報は標準出力を通して画面上に出力させ、 エラー情報やログメッセージ等は、ファイルに出力させるようにすれば、画面上にはシンプルな実行結果だけを表示させることができます。

また、標準出力は出力する情報がバッファリングされることが多いのに対し、 標準エラーはそうなっていないことが多いという違いもあります。

バッファリングというのは、データをある程度の量だけ蓄えておき、溜まってきたらまとめて処理するという方式のことです。 このとき、データを蓄えている場所をバッファと呼びます。

実は、入出力処理というのは、かなり処理時間が掛かるものであり、できることなら少ない回数で済ませたいものなのです。 そのため、バッファリングという方法を取ることによって、実際に入出力が実行される回数を減らすという方法を取る訳です。
バッファリングが行われている場合、ソースコード上の見た目では、数十回の入出力要求を出しているように見えても、 実はバッファに蓄えられているだけであって、まだ実行されていない可能性があります。

使用頻度の高い標準出力にとっては、この方式は有効に働くことが多いのですが、標準エラーの場合はそうとは限りません。 標準エラーは、今まさにプログラムは実行不能に陥って強制終了しようとしている状況で使われるかも知れませんから、 即時、出力処理が実行された方が都合が良いのです。

ファイルパス

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

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

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

相対パスによる表記は、現在注目しているディレクトリ(これを、カレントディレクトリと言います)を起点として、そこから目的のファイルまでの経路で書き表す方法です。 先ほどの絶対パスの例と同じファイルを、相対パスで指定することを考えます。
Windows で、カレントディレクトリが「C:\Program」だとすれば、「test\test.c」という表記になります。 OS X の例では、カレントディレクトリが「/Users/myname/Desktop」だとすれば、「test/test.c」になります。

fopen関数に渡すファイル名を単に「test.c」のように書いたとすれば、 それは実行ファイルのあるディレクトリがカレントディレクトリであるとみなされ、そこからの相対パスであると判断されます。 もし、実行ファイルが「C:\Program\test\bin」にあるとすれば、「C:\Program\test\bin\test.c」のことを指している訳です。

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

ここまでの例から分かるように、 ディレクトリ名の区切りの部分が、Windows では「\」になりますが、OS X では「/」を使います。 ただし、Windows であっても大抵の環境では「/」も使えますから、今後はソースコード上では「/」で統一します。 もし、「\」を使う場合、C言語の文字列表現のルール上「\\」のように重ねて表記しないと、エスケープ文字として扱われてしまうことに注意が必要です

相対パス表記の場合、カレントディレクトリよりも上位のディレクトリにあるファイルを指定したい場合に、記述に困ってしまいます。 そこで「..」という特別な文字列を用いて、1つ上のディレクトリを表現します。 また、「.」のようにピリオド1つだけの場合には、カレントディレクトリを意味します
このような特別な表記を使えば、カレントディレクトリが「C:\Program\test\bin」で、目的のファイルが「C:\Program\test\test.c」のときに、 「..\test.c」という相対パス表記が使えます。 この表記は、「カレントディレクトリから1つ上の階層にある test.c」を意味しています。
より詳細に、「.\..\test.c」と書くこともできますが、先頭の「.\」には意味がないので、記述を省略することが多いでしょう。

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

ファイルを扱う際の注意

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

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


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

まず、ファイルは複数のプログラムの共有資源であることを忘れないで下さい。 例えば、メモ帳などで開いているテキストファイルに対して、自分のプログラムから書き込み操作を行ったら、 メモ帳の側にとってみれば、突然、ファイルの中身が書き変わることになります。

また、あるアプリケーションで書き込み操作を行い、他方で読み込みを行っていれば、 読み込むタイミングによってファイルの内容が異なる可能性がありますし、 2つのアプリケーションが1つのファイルに対して書き込み操作を行えば、後から書き込んだ方のデータで前のデータが上書きされて消えてしまうかも知れません。

他にも、指定されたファイル、あるいはファイルパスは、既に削除されたり、名前を変更されたりして、存在しない可能性もありますし、 書き込み操作を行おうとしても、読み取り専用属性が付加されていて書き込めないかも知れません。

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


練習問題

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

問題A fopen関数の第2引数が "w" の場合と、"a" の場合との挙動の違いを確かめて下さい。

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

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


解答ページはこちら

参考リンク

更新履歴

'2017/5/11 FILE周りの用語について、表現を見直した。

'2015/8/29 OS X に対応した記述を追加。
flose関数の戻り値もチェックするようにした。

'2014/1/16 OS X に対応。

'2010/9/12 「FILE構造体」という表記を「ファイル構造体」に改めた。

'2010/5/15 エラー発生時には、exit関数で失敗終了させるようにサンプルプログラムを修正。

'2010/5/4 オープンモードについての解説に、指定ファイルが存在しない場合の挙動についての説明等を追加。

'2010/4/30 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ