第37回
ファイルの扱い(2)~ファイル操作の実例

オープン処理のいろいろな書き方

ファイルのオープン処理は、他の処理に比べてエラーの発生する可能性が高くなります。エラーやうっかりミスによるバグの防止に配慮しなければなりません。

オープン処理にエラーは付きもの

fopen関数でファイルをオープンする場合、対象となるファイルが必ず存在しているとは限りませんし、仮に存在していても、指定したアクセスモードでオープンできるかどうかわからないこともあります。

プログラムの中で常に名前も場所も決まった特定のファイルをオープンし、さらにそのファイルが確実に存在しているのならば問題はないでしょう。しかし、そのようなプログラムは非常に稀(まれ)です。

たとえば、ネットワーク上であるプログラムがファイルを排他的にオープンしていた場合、他のプログラムが同じファイルを書き込みモード("w")でオープンしようとしてエラーになる場合があり得ます。

ユーザーがファイル名を自由に設定できる仕様のプログラムで、パスやファイル名が間違って指定されたためにオープンできない――といったことも起こり得ます。

データファイルはプログラム内の変数などとは異なり、プログラムとは別に存在します。そのため、プログラムはオープンするファイルの状態を事前に知ることはできません。ファイルのオープンにはエラーが付きものです。そのため、ファイルをオープンする処理では、エラーとなった場合への対処が必須になります。

必ずfopenの戻り値を調べる

fopen関数では、ファイルをオープンできなかった場合にNULLポインタが返ってきます。プログラムではfopen関数の戻り値がNULLかどうかを調べ、NULLだった場合にはエラーに対処する処理を実行しなければなりません。

シンプルなプログラムでは「ファイルが見つかりません」とか「ファイルのオープンに失敗しました」といった内容のメッセージを表示し、処理を終了する形でも構いません。大規模な業務システムでは、
ファイルが存在しなければ新規に作成する
ファイルがほかのプログラムでロックされていれば
ロックが解除されるまで待つ
――といった対処が必要になるでしょう。

オープン~エラーの判定を1行にまとめる

ファイルのオープン処理では、前回紹介したように「fopen関数の実行」「FILE構造体型変数への戻り値の代入」「戻り値がNULLかどうかの判定」の3つの処理を1行にまとめて記述するのが一般的です。

if ((fp = fopen("abc.txt", "r")) == NULL) {
                : (エラー時の処理)
} else {
                : (成功時の処理)
}
といった書き方をします。

シンプルなプログラムではエラーメッセージを出す程度なので、エラー時の処理を先に記述する方がわかりやすくなります。もちろん以下のように、成功時の処理を先に書いても構いません。
if ((fp = fopen("abc.txt", "r")) != NULL) {
                : (成功時の処理)
} else {
                : (エラー時の処理)
}

“=”が1個か2個かで大違い?!

複数の処理を1行にまとめると、いかにもCらしい簡潔なソースコードになります。しかし、この書き方はバグを読み込む可能性も秘めています。

if ((fp = fopen("abc.txt", "r")) == NULL) {
上のように記述した場合、最後のNULLと比較するところをうっかり

if ((fp = fopen("abc.txt", "r")) = NULL) {
のように記述してしまう場合があります。これは明らかに入力ミスなのですが、コンパイル時にエラーとはなりません。上の式は、“fp = fopen("abc.txt", "r")”というファイルをオープンする処理の結果である変数fpに定数のNULLを代入する――という意味になります。

プログラマーの意図する「ファイルをオープンした結果である変数fpがNULLと等しい」という意味とはまったく異なっていますが、式自体は文法として間違ってはいません。文法として間違ってはいないため、コンパイルエラーとはならないのです。

このように間違えてしまうと、FILE型構造体の変数fpにNULLが代入されることになります。このときifで判定するのはfpの値ではなく、「fpにNULLを代入した結果」です。その式の評価は常に「真」です。すると、fpの値がNULLであるにもかかわらず、ソース上では「ファイルのオープンに成功した場合の処理」が実行されてしまいます。続く処理に支障をきたすことは明白です。

NULLを左辺に置けば安心

こういったソースの記述ミスによるバグを防ぐには、以下のように記述するとよいでしょう。
if (NULL == (fp = fopen("abc.txt", "r"))) {

fopen関数の結果をNULLと比較するのではなく「NULLという定数をfopen関数の戻り値と比較」するのです。このように記述するクセを付けておけば、以下のように“==”を“=”と間違えて記述した場合に「定数に値を代入することはできない」ため、必ずコンパイル時にエラーとなり、ミスに気付きます。
if (NULL = (fp = fopen("abc.txt", "r"))) {

ファイルのオープン処理に限らず、関数の戻り値などが定数と等しいかどうかを調べる場合、常に定数の方を左辺(演算子の左側)に記述するようにしておけば、コンパイルの段階で“==”と“=”の入力ミスに気付くことができます。

一般的なCのソースを見慣れていると少し違和感を覚える書き方ですが、慣れると案外わかりやすい書き方でもあります。