プログラムが計算したものは、書き出さないかぎり終了と同時に消えてしまいます。C言語のファイルインターフェースは <stdio.h> - printf と同じヘッダー - にあり、意図的に似た作りになっています。ファイルはバイトのストリームであり、画面に対して働く関数には、ストリームを引数に取る双子がいます。
ハンドルの型は FILE * です。FILE の中身を覗くことはありません。これは不透明な型(typedef 参照)で、常にそのポインタだけを持ちます。
3つのステップ
すべてのファイル操作は同じ形をしています。開く、使う、閉じる。
NULL チェックは、任意の心配性ではありません。fopen は、(読み込みモードで)ファイルが存在しない、ディレクトリが書き込み不可、パスが間違っている、プロセスのファイルハンドルが尽きた - そのいずれでも失敗し、どの場合も NULL を返します。NULL のハンドルを使うのはセグメンテーション違反です。
エラーを stdout ではなく stderr に送るのが慣習です。バッファリングされず、本来の出力とは別にリダイレクトできるからです。
fopen のモード
第2引数は短いモード文字列です。"w" が既存ファイルを黙って空にするため、これを間違えるのがこの分野で最も破壊的なミスです。
| モード | 読み | 書き | ファイルが存在する場合 | 存在しない場合 |
|---|---|---|---|---|
"r" | 可 | 不可 | 先頭で開く | 失敗、NULL を返す |
"w" | 不可 | 可 | 空に切り詰める | 作成される |
"a" | 不可 | 可 | 末尾にのみ書く | 作成される |
"r+" | 可 | 可 | 先頭で開く | 失敗、NULL を返す |
"w+" | 可 | 可 | 空に切り詰める | 作成される |
"a+" | 可 | 可 | どこでも読めるが、書くのは末尾 | 作成される |
これらのいずれにも b を付ける("rb"、"wb"、"ab+")とバイナリモードになります - 詳しくは最後に。
実際のデータ損失を防ぐ2つのルール:
- 読むつもりなら
"r"を使う。 打ち間違えたファイル名に対する"r+"は安全に失敗し、"w+"は空のファイルを作るだけで失うのは時間くらいです。しかし"r"のつもりで正しいファイル名に"w"を使うと、データが破壊されます。 - ログには
"a"を使う。 ストリームの位置がどこにあっても、すべてのfprintfは末尾に着地します。ログが求めるのはまさにそれです。
1つのプログラムで書いて読む
下のエディタは完全な往復を実行します - ファイルを作り、レコードを書き込み、閉じ、読み込み用に開き直して、見つけたものを表示します。
fprintf と fscanf は、第1引数にストリームを取る printf と scanf です。書式指定子まわりはすべて同一で、name のあふれを防ぐ %31s の幅指定も含まれます。
ループの条件は == 2、つまり書式が要求する項目の数です。代わりに EOF と比較するのは古典的なバグです。おかしな行があると fscanf は EOF ではなく 0 を返すので、消費できない入力の上でループが永遠に回り続けます。
fgets で1行ずつ読む
fscanf は、かっちり整形されたデータには便利です。テキストファイル - 設定、ログ、CSV、人が書いたもの全般 - では、行全体を読みましょう。次のループが定番です。
fgets が正しいデフォルトである理由:
- バッファサイズを受け取るので、オーバーフローしません。
sizeof lineを渡しておけば、配列のサイズを変えても呼び出しは正しいままです。 - ファイル終端やエラーで
NULLを返すので、ループの条件としてきれいです。 - 行がバッファに収まったときは改行を残します。これは有用で、得られたものに
'\n'がなければ、その行はバッファより長く、残りがまだ待っているという意味です。strcspn(line, "\n")は改行の位置(なければ文字列長)を返すので、そこに'\0'を代入すればどちらの場合も正しく切り詰められます。
本当のファイル終端とエラーを区別するには、ループの後で問い合わせます。
if (ferror(in)) {
fprintf(stderr, "read error\n");
} else if (feof(in)) {
/* 正常終了 */
}
ループの条件に while (!feof(fp)) と書いてはいけません。feof は読み取りがすでに失敗した後にしか真にならないので、そのループは最後のバッファの内容を1回余分に処理します。代わりに読み取り関数の戻り値をテストしましょう - 上の2つのループがそうしているように。
1文字ずつ:fgetc と fputc
バイト単位の仕事 - 文字数を数える、ファイルを変換する、コピーする - には、1呼び出しで1文字を扱う fgetc と fputc があります。
重要な細部が1つ。c は char ではなく int として宣言されています。fgetc が int を返すのは、あらゆるバイト値に加えて別個の番兵 EOF(-1)を返せるようにするためです。これを char に格納すると、プラットフォームで char が符号付きかどうかによって、EOF との比較が常に偽になるか、バイト 0xFF に対して誤って真になります。これはC言語で最も古い落とし穴の1つです。
エラーを正しくチェックする
実運用での読み込みはこうなります。
errno は直近の失敗を表すコードを保持し、strerror はそれを文になおします。perror は自分のメッセージとその文を1回の呼び出しで表示します。両者にはそれぞれ <errno.h> と <string.h> が必要です。
fclose も失敗しうる - バッファされたデータをフラッシュするので、書き込みがディスクに収まらないことがある - ため、重要なものについてはチェックしましょう。
if (fclose(fp) != 0) {
fprintf(stderr, "failed to flush and close\n");
}
バイナリモードを1段落で
テキストモードは改行を変換することがあり(Windowsでは書き込み時に \n が \r\n になり、読み込み時に戻ります)、特定のバイトを特別扱いすることもあります。テキストでないデータ - 画像、そのままダンプした構造体、圧縮されたかたまり - は b を付けて開き、生のバイトを移動させる fread/fwrite を使いましょう。
fwrite(ptr, size, count, fp) は size バイトの項目を count 個書き込み、実際に書けた個数を返します。fread はその鏡写しです。ただしこの方法で書かれたファイルは、書いたマシンに縛られている点に注意してください - 構造体のパディング、整数のサイズ、バイト順がすべてバイト列に漏れ出します - ので、キャッシュや一時ファイルには適していますが、ほかのプログラムが読む必要のあるフォーマットには不適切です。
よくある間違い
fopenのNULLをチェックしない。 その後のクラッシュは、オープンではなく読み込みのせいにされます。"r"のつもりで"w"で開く。 気づく前にファイルが空になっています。fcloseを忘れる。 バッファされた出力が失われ、閉じられないファイルごとにハンドルが漏れます。while (!feof(fp))。 最後の行を2回処理します。代わりに読み取りの呼び出しをテストしましょう。char c = fgetc(fp)。EOFとの比較が壊れます。intを使いましょう。- 幅なしの
fscanf("%s", buf)。 キーボードに対する scanf とまったく同じバッファオーバーフローです。 - 相対パス。
fopen("data.txt", "r")は実行ファイルの隣ではなく、カレントディレクトリを探します。ファイルが「消えた」ときは、たいていこれが原因です。
よくある質問
C言語でファイルを開くには?
FILE *fp = fopen("data.txt", "r"); は読み込み用にファイルを開き、FILE * のハンドルを返します。失敗した場合は NULL です。ハンドルを使う前に必ず NULL をチェックし、終わったら fclose(fp) を呼びましょう。
C言語の fopen のモードにはどんなものがありますか?
"r" は読み込み(ファイルが存在する必要あり)、"w" は書き込み(作成、または既存ファイルを空に切り詰める)、"a" は追記(作成し、末尾に書く)。+ を足すとそれぞれ読み書き両用になります:"r+"、"w+"、"a+"。b を足す("rb"、"wb")とバイナリモードで開きます。
C言語でファイルを1行ずつ読むには?
while ループの中で fgets を使います:while (fgets(line, sizeof line, fp) != NULL) { ... }。改行ごと、またはバッファが一杯になった時点で止まり、ファイル終端で NULL を返します。バッファサイズを渡すのでオーバーフローしません。
C言語で書き込んだのにファイルが空なのはなぜですか?
たいていは fclose を忘れています。出力はバッファリングされるので、プログラムが異常終了するとデータがまだメモリに残っていることがあります。fclose はフラッシュして閉じ、fflush(fp) は閉じずにフラッシュします。もう1つの原因は、2度目に "w" で開いてしまい、今書いたファイルを切り詰めてしまうことです。