[前へ]   [目次へ]   [次へ]

ファイルを「開く」と「閉じる」(C/C++)


今回からファイル制御についてです。

C/C++でファイル制御を行う方法は通常ひとつではないのですが、
今回はC標準ライブラリのファイル制御関数群を使って解説します。

Windows上でのC標準ライブラリでは通常、
テキストアクセスとバイナリアクセスが分けられています。

この二つの違いは主に改行文字の取り扱いについてで、
テキストアクセスでは \r\n と \n が自動的に変換され、
バイナリアクセスでは何も変換が行われないということになります。
他にも違いがないわけではありませんが通常は問題になりません。


さて、C標準ライブラリを始め、ファイル制御は通常ハンドルを用いて行われます。
このハンドルはライブラリによって「ファイルポインタ」や
「ファイルディスクリプタ」や「ファイルハンドル」などと呼ばれます。

なお、C標準ライブラリの場合は FILE* を使うことから「ファイルポインタ」と呼ばれます。

そこでまずはハンドルの取得、解放を行う二個の関数を解説します。
なお、ここでいう「開く」「閉じる」とは、
ファイルへのアクセス権を取得することを「開く」、
ファイルへのアクセス権を解放することを「閉じる」と言います。
一般的なアプリケーションレベルでの「開く」「閉じる」とは意味が異なるので注意してください。

また、カレントディレクトリとは相対パスの基準となるフォルダのことで、
コンソール(コマンドプロンプトなど)から起動した場合は現在位置になりますが、
エクスプローラから直接起動した場合はWindowsのバージョンによって
CドライブだったりマイドキュメントだったりWindowsフォルダだったりとまちまちなので注意が必要です。

●fopen


プロトタイプ宣言:
FILE* fopen(const char *filename,const char *mode)

filename で指定したファイル名を持つファイルを開き、操作するためのハンドルを返します。
この時、 mode の指定によってファイルの扱いが決定されます。

同時に開くことができるファイル数はOSや利用する標準ライブラリによって異なります。
同時に開くことができるファイル数は FOPEN_MAX の値で確認することができます。

既に自プログラムが開いている、または他のプログラムが開いているファイルを開くことができるかどうかは、
対象のファイルを開いた方法およびライブラリによって異なります。
ライブラリによってはファイルを開く時に「共有モード」を指定できるものがあり、
共有可能になっている場合は同時に同じファイルを開くことができます。
fopen 関数ではこの指定がないため、使用しているライブラリに依存します。

読み書き両用で開いたファイルでも、読み込みと書き込みを切り替える時は一度シーク(操作位置の再設定)を
行わなければならないとされています。(シークについてはもう少し先で解説します)
このことは案外忘れやすいので注意してください。

戻り値:指定したファイルを正しく開くことができた場合、そのファイルを操作するためのハンドルを返します。
   開くことができなかった場合、エラーが発生した場合は NULL を返します。

引数:
const char* filename :開く対象のファイル名をナル終端文字列で指定します。
   絶対パスまたは、カレントディレクトリからの相対パスを指定します。
   Windows環境ではプログラム起動時のカレントディレクトリの位置が起動方法などによって変化するため、
   プログラム上では原則として絶対パスを指定するようにした方が無難です。
   ソース上で直接指定する場合はパスに含まれる \ を \\ にエスケープすることに注意してください。
const char* mode :ファイルを扱うモードを指定します。
   以下のいずれかをナル終端文字列で指定します。(ライブラリによっては拡張されている場合もあります)
意味
r読み取り専用テキストアクセス。
対象のファイルがない場合は失敗します。
rb読み取り専用バイナリアクセス。
対象のファイルがない場合は失敗します。
w書き込み専用テキストアクセス。
対象のファイルがない場合は作成されます。
対象のファイルが既にある場合は中身が削除されます。
wb書き込み専用バイナリアクセス。
対象のファイルがない場合は作成されます。
対象のファイルが既にある場合は中身が削除されます。
a追記書き込み専用テキストアクセス。
対象のファイルがない場合は作成されます。
ファイルの終端からアクセスが開始されます。
ab追記書き込み専用バイナリアクセス。
対象のファイルがない場合は作成されます。
ファイルの終端からアクセスが開始されます。
r+読み書き両用テキストアクセス。
対象のファイルがない場合は失敗します。
rb+読み書き両用バイナリアクセス。
対象のファイルがない場合は失敗します。
w+読み書き両用テキストアクセス。
対象のファイルがない場合は作成されます。
対象のファイルが既にある場合は中身が削除されます。
wb+読み書き両用バイナリアクセス。
対象のファイルがない場合は作成されます。
対象のファイルが既にある場合は中身が削除されます。
a+追記書き込み+読み取りテキストアクセス。
対象のファイルがない場合は作成されます。
ファイルの終端からアクセスが開始されます。
ab+追記書き込み+読み取りバイナリアクセス。
対象のファイルがない場合は作成されます。
ファイルの終端からアクセスが開始されます。


●fclose


プロトタイプ宣言:
int fclose(FILE *stream)

fopen 関数を使って開いたファイルを閉じて、ハンドルを解放します。
この関数によって解放したハンドルは使用できなくなります。

なお、最初から定義されている stdin stdout stderr の各ハンドルを解放してはいけません。

戻り値:正常にファイルを閉じてハンドルを解放できた場合、0を返します。
   なんらかのエラーが発生した場合は EOF を返します。

引数:
FILE* stream :解放するハンドルを指定します。



Windows上では fopen したハンドルを fclose することなくプログラムが終了した場合は
Windowsがそのプログラムが開いていたファイルを全て閉じてくれます。
しかし、用が済んだハンドルはちゃんと fclose して解放するようにしましょう。(理由は前回の通りです)


さて、これだけだと何も意味がないのですが、一応使用例を示します。
これを実行するとカレントディレクトリのフォルダに中身が空のtest.txtができます。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
#include <stdio.h>

int main(void){
   FILE *fp;
   
if((fp=fopen("test.txt","w"))==NULL){//(1)ファイルを開きます
      
puts("ファイルが作れませんでした。");//(2)失敗時の処理を書きます
      
getchar();
      
return 0;
   }
   fclose(fp);
//(3)ファイルを閉じます
   
return 0;

そのままコンパイルして実行すると「どこか」にtest.txtができるわけですが、ちょっとそれも困ります。
なのでWindowsアプリで使うなら、
自分のEXEのある場所を調べてそれを基準にファイルパスを生成したり、
絶対パスでファイル名を取得する処理などが必要となります。

また、Visual C++ を使っているのであれば、
デバッグ実行時はデフォルトでカレントディレクトリは「ソースのあるフォルダ」になっているはずですので、
上のようなコードを比較的気楽に書くことができます。

さて、ここで重要な注意が一つあります。
それは ファイル操作をする時は必ず「コピーしたファイル」を使ってデバッグする ということです。
というのも、読み込みのつもりでうっかり書き込みモードで開いたりすると、そのファイルの中身がなくなってしまいますし、
ファイルを書きかえた場合にバグによって正しく書き込まれずに壊れてしまうかもしれません。

もし一つしかないファイルを使ってそんなミスをするとファイルを失ってしまうので、
事前にコピーを作り、作っているプログラムのバグでそのファイルを破壊してしまっても大丈夫なようにしておきます。

通常のファイルに処理を適用するのはデバッグが終わってもう大丈夫という自信がついてからにした方が無難です。

それでは先ほどのコードのチェックポイントを見ていきます。

(1)5行目で、ファイルを書き込みテキストモードで開きます。
その結果返されるハンドルを fp に代入して、 NULL (失敗)かどうかを調べるコードです。

1 A  B C     D E            F   D' 1' G  H
( fp = fopen ( "test.txt" , "w" )  )  == NULL


1〜1'の()は式の優先を指定するだけの()で演算子ではありません。

通し記号種別
1複合式
G演算子(==)(論理、優先9、結合→)
H-0x00000000(NULL)定数
注:NULLの型は厳密には決まっていません。
      「どんなポインタ型にも暗黙に変換できる」という特別扱いになっています。
      Cではvoid*であれば大丈夫だったのですが、
      C++ではvoid*からの暗黙変換が禁止されているため、特別扱いされています。


複合式を先に展開します。

A  B C     D E            F   D'
fp = fopen ( "test.txt" , "w" )


通し記号種別
AFILE*不定(未初期化)変数(fp)
B演算子(=)(算術、優先2、結合←)
CFILE*(*)(const char*,const char*)0x004016e0関数(fopen)
D演算子( () )(その他、優先16、結合→)
Econst char[9]"test.txt"文字列定数
Fconst char[2]"w"文字列定数
D'演算子Dの終点

優先順位から、演算子D(左辺C、引数E、F)により fopen 関数が呼び出され、
指定したtest.txtという名前で書き込みテキストモードで開き、それを操作するハンドルを返します。

A  B C
fp = vtemp1


通し記号種別
AFILE*不定(未初期化)変数(fp)
B演算子(=)(算術、優先2、結合←)
CFILE*0x00426a90一時変数

後は普通に代入され、最初の式に戻ります。

A       G  H
vtemp2  == NULL


通し記号種別
AFILE*&0x00426a90一時変数(参照先:fp)
G演算子(==)(論理、優先9、結合→)
H-0x00000000(NULL)定数

ここで NULL は FILE* へ暗黙キャストされ、比較されます。
この条件式は fp が失敗を意味する NULL になった場合は真になります。

(2)6行目の部分にはファイルを開くことに失敗した場合の処理を書きます。
今回のサンプルではエラーメッセージを出して終了しています。

(3)10行目で fclose 関数を呼び出し、 fp に取得したハンドルを解放しています。
これが成功すると fp に入っている値のハンドルは無効になり、ファイルへのアクセスが終了します。
この呼び出し以降はどのような方法で開いたにせよ、再び該当ファイルを開けるようになります。

ただし、ファイルを閉じた直後に直ちに開き直そうとしたり(ソース上で fclose の直後に同じファイルを fopen するなど)、
大量のファイルを一斉に開こうとした場合などは、本来開けるはずの条件であっても失敗することがあります。
おそらくWindowsとかアンチウィルスとかの都合だと思われますが、成功したり失敗したりするので、
こういった条件に該当しそうな場合は一度失敗しても、少し待ってからやり直してみるようにしておくといいです。


次回はテキストファイルからデータを読み出す方法についての予定です。

[前へ]   [目次へ]   [次へ]

プログラミング講座 総合目次

最終更新 2010/12/03