ホーム < ゲームつくろー! < C++踏み込み編 < ファイル入出力をごっちゃにしないために


その5 ファイル入出力をごっちゃにしないために



 C++(Visual C++)にはファイル入出力の方法がいっぱいあります。まだそれ程プログラムの経験が無かった時には、「何でこんなにあるんだー。どれ使えばいいんだー!」と迷ったものです。同じ思いを抱いている人はきっと少なくないはずです。そこで、ここではファイル入出力方法についてまとめてみました。



@ カテゴリ(分類)がもうごっちゃです

 現在、VC++で扱えるファイル入出力関連は以下の5つに分類できます(MFCを除く)。

カテゴリ ヘッダーファイル
・ Cランタイムライブラリ
      低水準入出力関数 io.h
      ストリーム入出力関数 stdio.h
・ Cランタイムライブラリ(セキュリティ強化版)
      低水準入出力関数 io.h
      ストリーム入出力関数 stdio.h
・ C++iostreamクラスライブラリ fstream.h
・ 標準C++iostreamクラスライブラリ fstream
・ Win32APIファイル入出力 -


 基本的には下に行くにつれてWindowsプラットフォームへの依存性が高くなります(例外もありますので注意)。以下に簡単な説明を述べておきます。

 Cランタイムライブラリは、C言語で扱われていたファイル入出力の方法をサポートしますが、VC++等では表記の方法が少し変わっています。例えばCの関数であるopen関数の使用は推奨されなくなり、変わりに_open関数となっています(open関数でもコンパイルは出来ますがお勧めしません)。Cランタイムライブラリはさらに「低水準入出力関数」と「ストリーム入出力関数」に分かれます。低水準とストリームの違いは入出力に関して「バッファ」を扱うか否かで、ストリームの方がバッファを利用し高速に読み書きができます。バッファを使用せずOSを直接叩くのが低水準の方で、その分使い方が難しくなっています。

 Cランタイムライブラリ(セキュリティ強化版)は、従来のランタイムライブラリよりも頑健になったバージョンです。これはVisual Studio 2003の頃辺りからちらほらと出現して来ました(ちゃんと調べていませんので嘘かもしれません(^-^;)。特にエラーの通知が強化されており、より安全にファイル入出力ができるようになりました。Cランタイムを使用するのであれば、これからはこちらを強く推奨します。

 C++iostreamクラスライブラリは、C++でのファイル入出力において事実上の標準(ディファクトスタンダード)となっています(MSDNより)。Cランタイムライブラリと違いこちらは「クラス」なのでオブジェクト指向に基づいたファイル入出力をサポートします。このクラスライブラリを使用するにはfstream.hをインクルードします。ちなみに、こちらは「旧版」と呼ばれておりまして、色々調べたところ、どうやらVisual Studio .Net 2003以降では使用できなくなったようです。確かに、VS2005環境ではヘッダーファイルすらありません。

 標準C++iostreamクラスライブラリは、こちらもC++でのファイル入出力の1つで、オブジェクト指向に基づいたファイル入出力をサポートしています。上のC++iostreamクラスライブラリとの目に付く相違点は「名前空間」を持っていること、そしてヘッダーファイルがちょっと違うということです。どうして非常に似たクラスライブラリが提供されているのか、その本当の理由を私は知りませんが、多分名前空間によってライブラリ間の名前衝突を防ぐという現在のプログラミングスタイルに合わせる必要があったからかもしれません。上のC++iostreamライブラリは名前空間を持たないので、関数名が衝突してしまう可能性を含んでいます。双方の能力はほぼ一緒です。

 Win32 APIファイル入出力はプラットフォームSDK(Standard Development Kid)で定義されている入出力関数群です。当然のことながらプラットフォーム依存があります。Windows専用ということであれば、これを使っても問題はありません。


 これらのライブラリ群をのどれを使えばいいのか?そこが悩みどころであり、知りたいところです。
 オブジェクト指向をするまでも無い小さなプログラムの場合は、Cランタイム系で十分です。ただし、少なくともCランタイムライブラリを使うよりかはセキュリティ強化版を使用した方が良いと思われます。ただ、VC++6辺りだと強化版が使用できないかもしれません(確認はしていません)。
 C++言語でプログラムを組むのであれば、標準C++iostreamクラスライブラリの方が名前空間が使える分安全と言えるでしょう。Windows限定であればWin32APIの選択もありです。
 総合的には、移植性、使い勝手、そして将来性から判断して、標準C++iostreamクラスライブラリがベストでしょうね。そういう理由から、沢山あるファイル入出力の中から、標準C++iostreamによるファイルの読み書きを以下で説明します。



A テキストファイルとバイナリファイルの読み書き

 ファイルには「テキストファイル」と「バイナリファイル」があります。双方の違いは、言うまでも無いと思うのですが、テキストファイルが人が読める文字を組み合わせて作成されるファイルフォーマット、バイナリファイルは人が読めないバイト単位で表される数字の塊によるファイルフォーマットです。

○ テキストファイルの入力

 テキストファイルの入力に関して特に知っておきたいのは「ブロック単位での入力」です。標準iostreamクラスライブラリには、指定のデリミタ(区切り文字)で区切りながら行単位でテキストを取得するistream::getline関数およびistream::get関数が用意されています。

#include <fstream.h>
using namespace std;

int main()
{
   // Test.txtには「あいうえお,かきくけこ」
   // という文字列が入っているとします。

   char str1[101], str2[101];
   ifstream ifs1, ifs2;

   ifs1.open( "Test.txt" );
   ifs1.getline( str1, 100 , ',');   // 「あいうえお」がstr1に格納される。ファイルポインタがデリミタの先に移動
   ifs1.getline( str2, 100), ',');   // 「かきくけこ」がstr2に格納される

   ifs2.open( "Test.txt" );
   ifs2.get( str1, 100, ',');   // 「あいうえお」がstr1に格納される。ファイルポインタがデリミタの直前に移動
   ifs2.get( str2, 100, ',');   // 何も格納されない!!

   ifs1.close();
   ifs2.close();
}

 双方ともデリミタは格納しません。そして文末にナル文字(0x00)が付記されて文字列が格納されます。双方の違いは文字列取得後のファイルポインタの置き場所です。getline関数はテキストを取り出した後ファイルポインタをデリミタ文字の先に置くのに対して、get関数はデリミタ文字の直前にファイルポインタを置きます(下図参照)。この違いに注意しないと、思わぬバグに悩まされます。

 デフォルトのデリミタはCR+LFである「\n」ですが、ユーザの好きに出来ます。上の例では「,」をデリミタ文字に使いました。ただ、日本語のような2バイト文字はデリミタとして扱えませんので注意です。
 getline関数とget関数を使う場合には、文字列を格納する空の文字配列が必要です。配列の要素数を確定するために、本当なら次のデリミタまでの文字数を得られれば便利なのですが、さすがにそれをチェックする関数はありません。しかし難しく考えなくて良いのであれば、十分な要素数を持ったローカルな文字列配列を用意しておいて、それにいったん文字列を格納し、strlen関数で文字数をチェック、その後改めて必要な文字列配列を動的に作成すれば無駄は最小限です。

 
○ バイナリファイルの入力

 バイナリファイルの入力にはistream::read関数を用います。この関数は指定の数だけバイナリファイルからバイトデータを取り出します。この時デリミタ文字という概念はありません。バイナリファイルは純粋にバイト単位でデータをがんがん取れるのです。

#include <fstream.h>
using namespace std;

int main()
{
   // BTest.txtはバイナリファイルだと思いねぇ

   int num[10];
   ifstream ifs;

   ifs.open( "BTest.txt" );
   ifs.read( num, sizeof(int)*10 );   // 整数を10こ格納

   ifs.close();
}

 数値データはバイナリファイルにした方が圧倒的に便利です。ファイルに格納されているデータをバイトデータとしてダイレクトに変数に格納できるのですから、テキストを数値に変換して云々という手間がまったく無いんです。上のプログラムはその典型例です。10個の整数配列に直接データを取り込んでいます。この時、変換による数値の劣化のようなものも当然ありません。バイナリファイルは大変すばらしい保存方法なんです。

 「でも、バイナリファイルって人が読めないから扱いにくいよ」とおっしゃる方!バイナリファイルの中身をぜひご覧になってください。バイナリファイルはちゃんと読めます。例として、

BYTE b = 0xaa;
WORD w = 0x1234;
DWORD dw = 0xabcdef00;
char str[] = "TEST";

というデータをこの順番でバイナリファイルに格納すると、次のようになります。

変数 b w dw str
保存データ AA 34 12 00 EF CD AB 54 45 53 54 00
0x54 = "T", 0x45="E", 0x53="S"です

2バイト以上の変数は「リトルエンディアン」で格納されているので、1バイトずつ逆から読んで下さい。文字列は「1バイト変数の配列」ですから、リトルエンディアンにはなりません。ちゃんと「そのまんま」格納されているのがわかると思います。ですから、バイナリファイルにメモリを丸ごと出力(ダンプ)するという荒業なんかも出来ます。

○ その他の入力関数

 残りの関数についてざっと紹介します。

 iostream::gcount関数は、直前の入力された文字の大きさをバイト単位で返します。先程文字数をstrlen関数で取得すると説明しましたが、この関数の方が確実ですね。

char str[100];
ifstream ifs;
ifs.open( "Test.txt" );
ifs.getline( str, 100 );

int getsize = ifs.gcount();   // 入力文字のバイト数を取得


 iostream::ignore関数は、現在のファイルポインタの位置から指定の文字数(バイト単位)だけ読み進めます。格納はしません。そのままだと使い道があまりなさそうに感じてしまいますが、この関数は「デリミタまで読み飛ばす」という嬉しい機能が付いています。これを使えばテキスト読み込みなどで、例えば「,(コンマ)」単位で読み飛ばしということもできます。

char str[100];
ifstream ifs;
ifs.open( "Test.txt" );   // 「あいうえお,かきくけこ」だとします

ifs.ignore(getline( INT_MAX, ',');   // 「,」まで読み飛ばし

ifs.getline( str, 100 );  // str = かきくけこ


 iostream::peek関数は、現在のファイルポインタの位置にある文字(バイト単位)を調べます。文字は戻り値に取り出されます。ただし、ファイルポインタの位置は変更しません。トークンアナライザなどで重宝するかもしれません。

ifstream ifs;
ifs.open( "Test2.txt" );   // 「123」だとします
c = ifs.peek();   // c = 49 ('1')
c = ifs.peek();   // c = 49 ('1')
c = ifs.peek();   // c = 49 ('1')
ifs.close();


 iostream::putback関数は、非常に面白い性質を持った関数です。この関数は、取得関数で何らかの格納を行う前に文字を付記します。例えば、「123」という文字が入ったファイルをオープンした後に、putback('a')などとすると、getline関数で取得される文字列は「a123」となります。ただ、ちょっと振る舞いが変わっていて、最初だけは1文字しか付記できません。これは初期にバッファが格納されていないためかもしれませんが、ソースを詳しく解析していないので良くわかりません。文字列取得を1回行った後であれば、複数文字の付記は可能です。たぶんですが、この関数は1文字だけ付記するために使う関数ではないかと思います。

char str[100];
ifstream ifs;

ifs.open( "Test2.txt" );   // 「123」だとします
ifs.putback('a');
ifs.getline(str, 100);   // 「a123」と格納
ifs.close();


 iostream::seekg関数iostraem::tellg関数は、それぞれファイルポインタの位置を操作し、その位置取得する関数です。seekg関数は現在の位置(ios_base::cur)、最初から(ios_base::beg)、最後から(ios_base::end)という3種類の初期位置からのオフセットを指定します。tellg関数は現在のファイルポインタの位置を整数で返します。文頭は0になります。これらを駆使すると後述しますランダムアクセスが可能になります。

ifs.open("Test3.txt" );         // 「0123456789」だとします
ifs.seekg( 3, ios_base::beg );  // 最初から3バイト分移動
int tell = ifs.tellg();         // tell = 3
ifs.seekg( -2, ios_base::end ); // 最後から2バイト分後退
tell = ifs.tellg();             // tell = 8
ifs.close();



○ テキスト・バイナリファイルへの出力

 ファイルへの出力にはiostream::write関数が使われます。テキストでもバイナリでも書き出しできます。それは、どちらのモードで書き込み用ファイルをオープンしたかで判断されます。本来書き込みは読み込みよりもディスクエラーを犯す可能性が高いため、パラメータを慎重に設定しなければなりませんが、iostreamクラスは相当に厳重なエラーチェックをしてくれますので、そういうエラーからユーザを解放してくれます。

 出力はまず「出力ファイルモード」を選ぶところから始まります。

出力モード 意味
ios_base::out 出力モードでオープン
ios_base::trunc 既存のファイルを初期化してオープン
ios_base::app 既存のファイルに追加してオープン
ios_base::binary バイナリファイルとしてオープン

 ファイルオープン時にこれらのモードを複数選択します。追加で開くのか初期化して開くのかは重要な問題ですから、気を付けて下さい。

 ファイルオープンに成功したら、write関数で書き込みます。

ofstream ofs;
// バイナリ出力モードで新規ファイルオープン
ofs.open("Test.txt", ios_base::out | ios_base::trunc | ios_base::binary );

ofs.write( "リテラル文字は直接OK", 20 );

int num = 100;
ofs.write( (char*)&num, size(int) );  // 文字以外ははchar*型に変換します

ClassA Obj;
ofs.write( (char*)&Obj, size(ClassA) );  // オブジェクトも大丈夫

ofs.close();


 ios_binaryを付けずにオープンすると、テキストモードになります。バイナリとテキストで何が違うかといいますと、ずばり「改行の解釈」です。バイナリモードだと改行(\n)は「0x0D (CR)」となりますが、テキストモードの場合は、「0x0D(CR), 0x0A(LF)」とラインフィールドが自動的に付記されます。


 iostream系によるファイル入出力は、それほど難しくありませんし、必要な機能はしっかりと盛り込まれています。これをさらに駆使すれば、独自のトークンアナライザも作成できます。賢く使っていきたいもんです。