ファイル入出力
 
(このコンテンツはメールマガジンの STL & iostream 入門に手を加えたものです。「 STL と iostream が使えるかのチェック」等はメールマガジンの方のページをご覧ください)
 
 ファイル入出力 ( #11 )
 
 ここまでは std::strstream という、文字配列への入出力を行う iostream クラスを見てきました。ですが、 iostream クラスはこれだけじゃありません。
 まずは「ファイル」への入出力を行うストリームクラスについて見てみましょう。ファイル入出力を行うクラスは std::fstream です。このクラスは std::strstream に非常に似た方法でファイルに入出力を行うことができます。
 次の例は「ファイルを開いて、文字列と整数を書き込む」というものです。
 
////////////////////////////////////////////////////////////////
//  ファイルを開いて、書き込みます。
#include <fstream>    // をインクルードしてください。

bool OpenAndWrite()
{
    std::fstream cFStrm
        ( "Data.txt"    // 開くファイル名。
        , std::ios::out );    // 書き込みモードでファイルを開きます。
    if( cFStrm.fail() )
        return false;    // ファイルを開けませんでした。

    cFStrm
        << "First:" << " " << 1 << std::endl
        << "Second:" << " " << 100 << std::endl;
    return true;
}

// 結果( Data.txt の中身)
First: 1
Second: 100

/////////////////////////////
	
 
 まず std::fstream 型の変数を作成します。
 コンストラクタの第1引数には開くファイル名を渡します。今回の例では Data.txt というファイル名にします。このファイルは、用意しなくても自動的に作成されます。また、すでに存在している場合には、元からあるデータはすべて削除されるのでご注意ください。
 第2引数は std::strstream の第3引数と同じ、「入出力フラグ」を渡します。今回は「書き込みモード」なので「 std::ios::out 」を渡します。
 
 この時点で、ファイルが開かれました。もしファイルがなくても、自動的に作成されているはずです。ただ、例えば「排他モードの同名ファイルがすでに存在する」場合などには開くことができません。開けなければファイルへの操作はできないので、開けたかどうかチェックします。
 チェックは std::fstream::fail() を呼び出すことで行います。このメンバ関数、 std::strstream でも #09 で出てきましたね。「入力に失敗したかどうか」のチェックで呼び出しました。これと同じものです。
 
 開けたら、あとは std::strstream と同じ方法で書き込みます。文字列でも実数でも、同じようにファイルへと書き込むことができます。ただ、文字列と違って「終端文字」は必要ないので、 std::ends を渡す必要はありません。
 
透明
透明
■ C++ : コンストラクタとデストラクタ ( #13 )
 今回はコンストラクタの補足と、デストラクタについて紹介します。
 以前紹介したコンストラクタの使用例は
 
class CWithConstructor
{
    int m_i;
public:    
    CWithConstructor( int p_i )
    {
        m_i = p_i;
    }
};
	
 
 みたいな感じでした。ですが、これは実は間違いです。もし、
 
class CWithConstructor
{
    const int m_i;
public:    
    CWithConstructor( int p_i )
    {
        m_i = p_i;
    }
};
	
 
 とメンバ変数が const の場合には、コンストラクタが呼ばれる前に初期化する必要があります。そのため、コンストラクタでは次のような構文を使用してメンバ変数を初期化します。
 
class CConstructorWithConst
{
    const int m_i;
public:
    CConstructorWithConst( int p_i )
        : m_i( p_i )                    // ここ。
    {}
};
	
 
 コンストラクタの関数名のすぐ次にある1行がそうです。この構文で初期化することで、 const なメンバ変数も初期化できます。
 
 もうひとつ、デストラクタというものについて紹介します。
 コンストラクタと対になるのが、デストラクタです。デストラクタは、自クラス型変数が消えてなくなるときに自動的に呼ばれるメンバ関数です。
 
class CWithDestructor
{
public:
    virtual ~CWithDestructor()
    {
        printf( "CWithDestructor\n" );
    }
};

void Use_CWithDestructor()
{
    CWithDestructor cWithDestructor;
}

// 結果
CWithDestructor
	
 
 戻り値がなく、クラス名の頭に ~ がついたメンバ関数がデストラクタとみなされます。このデストラクタは、変数がなくなるときに自動的に呼ばれます。ちなみに必ず仮想関数にする必要があります(仮想関数については #16 で解説します)。
 
  fopen() などの標準 C ライブラリを使ったファイル処理をしたことのある人は、 std::fstream の解説で「なんで閉じなくていいんだろう」と思ったことでしょう。これは、 std::fstream のデストラクタが自動的に呼んでくれているからなのです。
透明
透明
 
 さて同じく、今度はファイルから読み取ってみましょう。ファイルは先ほど書き込んだ Data.txt です。
 
////////////////////////////////////////////////////////////////
//  ファイルを開いて、読み取ります。
#include <stdio.h>
#include <fstream>    // をインクルードしてください。

bool OpenAndRead()
{
    std::fstream cFStrm
        ( "Data.txt"    // 開くファイル名。
        , std::ios::in );    //読み取りモードでファイルを開きます。
    if( cFStrm.fail() )
        return false;    // ファイルを開けませんでした。

    char chData1[130];
    int iData2;
    while( !cFStrm.eof() )    // ファイルの最後まで検索します。
    {
        cFStrm
            >> chData1    // 文字列を取り出して、
            >> iData2;    // 整数も取り出す。
        if( !cFStrm.fail() )
            printf( "%s: %d\n", chData1, iData2 );
    }

    return true;
}

// 結果
First: 1
Second: 100

/////////////////////////////
	
 
 まず、コンストラクタの第2引数が std::ios::in になっていますね。これは std::strstream と同様です。
 開けたかどうかチェックしたあと、 while ループでデータを次々と読み取っていきます。ループを抜けるチェックは std::fstream::eof() というメンバ関数を呼び出して行います。このメンバ関数は「 EOF (ファイルの一番最後)に到達していたらゼロ以外を返す」という機能を持っています。
 このメンバ関数がゼロを返し続ける間、文字列と整数値を読み取ります。うまく読み取れたかどうかは std::strstream の時と同じく std::fstream::fail() を呼び出してチェックします。
 
 以上のように、 std::fstream は std::strstream とほとんど同じように使用できます。ちなみに、 std::strstream がそうだったように「入力専用クラス」と「出力専用クラス」も用意されています。 std::ifstream と std::ofstream です。これらを使えば「入力か出力のどちらかだけ」行うことができます。そしてもちろん、 std::fstream はこのふたつを組み合わせたクラスなので、どちらもできるというわけです。
 
 オープンモードとバッファリング ( #12 )
 
 std::fstream を使うと、 std::strstream とほとんど同じ方法でデータの入出力を行えます。
 ですが、「ファイル」と「文字列」では少なからず違いが存在します。たとえば std::fstream::eof() は「ポインタがファイルの末端にあるかどうか」をチェックするメンバ関数です。これが std::strstream で機能することはありません(呼び出すことはできますが)。
 この「ファイル操作のみ」に関わってくるものをみっつほど紹介します。
 
 まずは「オープンモード」、コンストラクタの第2引数に渡すフラグについてです。フラグを変えることで、開くファイルに「どうアクセスできるか」を設定することができます。
  std::ios::in と std::ios::out もそのフラグのひとつです。このふたつのうち、どちらか一方は必ず立てる必要があります。
 
in : 入力用。ファイルが存在しないと fail() がゼロ以外を返す。
out : 書き込み用。ファイルがあれば全消去。なければ作成。
in | out : 入出力用。なければ fail() がゼロ以外を返す。
 
 特に std::ios::out が上書きではないことに注意してください。また std::ios::in と std::ios::out の両方を立てた場合には「 std::ios::in 書き込み機能が付いた」と考えた方がいいでしょう。
 
 これらのフラグと同時に立てることのできるフラグに std::ios::trunc と std::ios::app があります。このふたつのうちどちらか一方だけを立てられます。
 
trunc : まっさらな新規ファイルを開く。 out に近い。
app : 追加書き込み。書き込みは必ず一番最後へ。読み取りは制限なし。
 
  std::ios::trunc は std::ios::in と std::ios::out の両方を立てた時に「まっさらなファイル」を必要とする時に立てます。 std::ios::app は書き込みポインタを移動しても必ず最後に書き込まれるフラグです。このふたつのフラグは「書き込み」についてのモードなので、「 std::ios::in のみ」と同時に立てられないので注意してください。
 
 これらともまた別のフラグに std::ios::ate と std::ios::binary があります。これらは別々に任意に立てられます。
 
ate : ポインタがファイルの最後に来た状態で開く。ポインタは移動できる。
binary : データを「素の2進データ」として取り扱う。
 
  std::ios::binary はテキストファイル以外を操作するときに使います。
 以上のフラグについて、標準 C ライブラリの fopen() のフラグとの互換性表を載せておきましょう。
 
ios::in     "r"
ios::out    "w"
ios::out | ios::trunc    "w"
ios::out | ios::app      "a"
ios::out | ios::binary   "wb"
ios::out | ios::trunc | ios::binary    "wb"
ios::out | ios::app | ios::binary      "ab"
ios::in | ios::binary                  "rb"
ios::in | ios::out                     "r+"
ios::in | ios::out | ios::trunc        "w+"
ios::in | ios::out | ios::app          "a+"
ios::in | ios::out | ios::binary       "r+b"
ios::in | ios::out | ios::trunc | ios::binary  "w+b"
ios::in | ios::out | ios::app | ios::binary    "a+b"
	
 
バッファリング  ふたつめの違いは「バッファリング」です。
 ファイル出力の場合、メモリ上に書き込む場合と違ってファイルへの書き込みは時間的コストが非常にかかります。この場合、1行単位などでまとめてデータを書き込めれば、時間的コストを大幅に削減できます。
 そこで std::fstream を使った書き込みの場合、データを「バッファ」に蓄え、貯まったところで一気にファイルへと書き込む方法が取られます。この「まとめて書き込むこと」を「バッファリング」または「フラッシュ」と言います。
 
 バッファリングを行うには3つの方法があります。 std::endl を渡す方法、 std::flush を渡す方法、何もしない方法の3つです。
  std::endl は std::strstream でも使った「改行文字( '\n' )」を書き込むマニピュレーターですが、同時にバッファリングも行ってくれます。 std::endl を使うことで、1行毎にバッファリングが行われます。
 std::flush は、ただ単にバッファリングを行うマニピュレーターです。 std::endl と同じように << に渡すことでバッファリングを実行します。 std::endl の改行なしといったところでしょう。
 「何もしない」というのは、関数から抜けて「 std::fstream 型変数」が削除されたとき、自動的にバッファリングが行われるということです。バッファにデータが溜まったままファイルに書き込まれずに削除される、ということはありません。
 「 std::flush 」と「何もしない」を使ったのが次のプログラムです。
 
/////////////////////////////
//  std::flush() を用いてバッファリングを行います。
#include <fstream>

bool UseFlush()
{
    std::fstream cFStrm( "Data.txt", std::ios::out );
    if( cFStrm.fail() == true )
        return false;    //ファイルを開けませんでした。

    cFStrm
        << "Before...";
    // この段階ではファイルにはまだ書き込まれていません。
    cFStrm
        << std::flush;
    // やっと書き込まれました。
    cFStrm
        << "After";
    // これはこの関数から抜けたときに書き込まれます。
    return true;
}

// 結果( Data.txt の中身)
Before...After

/////////////////////////////
	
 
 といっても、バッファリングの「途中」を見るのはちょっと工夫が必要ですね……。
 
 ファイルポインタ ( #13 )
 
 文字列とファイルの違いの最後の部分「ファイルポインタ」について見ていきましょう。
  std::strstream の時には「書き込みポインタ」と「読み取りポインタ」のふたつを持っていて、それぞれ独立していました。 std::fstream の場合、少々事情が変わってきます。それは、「ファイルポインタ」というシステムによるものです。
 ファイルを開いたとき、自動的に「ファイルポインタ」というものが作成されます。これは変数などとして作成されるわけではなく、 OS が作成し管理するものです。
 ファイルポインタは普通のポインタと同じように、ファイル上の任意の点を指し示しています。読み書きを行えば自動的に進みます。また、関数を呼び出すことでこのポインタを移動することもできます。
 
ファイルポインタ   std::fstream は、ファイルが持つファイルポインタを使用して読み書きを行います。 std::strstream のようにポインタ変数を内部に持ちません。
 と言っても、その違いはそれほどありません。ポインタの進み方は同じですし、 seekp() や seekg() などのメンバ関数を使うことで自由にポインタを移動できます(というかそういうふうに作られています)。
 大きな違いのひとつは、読み取りも書き込みも「ひとつの」ポインタで行うということです。 std::strstream のように別々ではないので、「今書き込んだ文章を読み取る」ときには、ポインタを最初に持ってこなければなりません。なので、 std::strstream::seekp() と std::strstream::seekg() まったく同じように機能します。どちらを使っても同じってことです。
 では実際に、ファイルポインタを操作してみましょう。
 
////////////////////////////////////////////////////////////////
//  ファイルポインタを移動します。
#include <stdio.h>
#include <fstream>

bool MovePointer()
{
    std::fstream cFStrm( "Data.txt"
        , std::ios::out | std::ios::in | std::ios::trunc );
    if( cFStrm.fail() == true )
        return false;

    //    現在のふたつのポインタの位置を出力します。
    printf( "tp:%d,tg:%d\n", cFStrm.tellp(), cFStrm.tellg() );
    //    文字列を書き込みます。
    cFStrm
        << "ABCDEFG" << std::endl;
    //    再びふたつのポインタの位置を出力します。
    printf( "tp:%d,tg:%d\n", cFStrm.tellp(), cFStrm.tellg() );

    //    書き込み用のはずの seekp() で先頭に移動。
    cFStrm.seekp( 0 );
    char ch[130];
    cFStrm
        >> ch;    // 書き込んだ文字列を読み取ります。
    printf( "%s", ch );
    return true;
}

// 結果
tp:0,tg:0
tp:9,tg:9
ABCDEFG

/////////////////////////////
	
 
 この例では、新規にファイルを開き、書き込んだあと読み取っています。
  std::fstream::tellp() と std::fstream::tellg() の値が常に同じという点、本来書き込みポインタを移動させるはずの std::fstream::seekp() で読み取りポインタが移動できている点に注目してください。このように std::fstream では、読み取りポインタと書き込みポインタが同じように扱われます。これは、単なるポインタではなく「ファイルポインタ」を操作しているからなのです。
 
 しかし、それ以外の点については std::strstream と同じように操作できていることに気付くでしょう。前回の「オープンモード」や「バッファリング」も、操作する上ではほとんど気にならない差です。これが iostream の設計方針なのです。
(C)KAB-studio 2000 ALL RIGHTS RESERVED.