ファイルストリーム

ファイル読み書きの新しい方法

C言語ではファイルの読み書きはfopen関数(fopen_s関数)でファイルを開き、読み書き用の関数を呼び出し、最後にfclose関数でファイルを閉じる、という流れで行っていました。
もちろんC++でもこの方法でファイル読み書きが可能です。

C++では、新しいファイルの読み書き方法が提供されています。
ファイルの扱い方に関してはC言語と共通する部分も多くありますので、もし分からない場合は先にC言語の方のファイル処理に目を通しておくことをおすすめします。

出力ストリーム

まずはファイルの書き込みからです。
ファイルの書き込みにはofstreamを使用します。

#include <iostream>
#include <fstream>

int main()
{
    const char *fileName = "C:\\test.txt";

    std::ofstream ofs(fileName);
    if (!ofs)
    {
        std::cout << "ファイルが開けませんでした。" << std::endl;
        std::cin.get();
        return 0;
    }

    ofs << "あいうえお かきくけこ" << std::endl;
    std::cout << fileName << "に書き込みました。" << std::endl;

    std::cin.get();
}
C:¥test.txtに書き込みました。

C++流のファイル読み書きを使用するには、先頭に#include <fstream>が必要です。

このコードを実行すると「C:\test.txt」というファイルが新しく作成されます。
(存在していた場合は上書き)
ファイルを開いて中身を確認してみてください。

アクセス権限の関係でCドライブ直下にファイルが書き込めない場合はパスを適当に変更してください。

パスの指定は相対パスでも可能です。
その場合、ファイルは実行ファイルと同じ場所に作成されます。
(Visual Studioのプロジェクトフォルダ内)

この方法によるファイルの書き込みは、「std::cout」によるコンソール画面への文字出力とほぼ同じであることに気づいたと思います。
実際のところ、出力先がコンソール画面からファイルに変わっただけです。

ofstream出力ファイルストリームの機能を提供するクラスです。
(「o:アウトプット」の「f:ファイル」「stream:ストリーム」)
fopen関数でファイル構造体のポインタを通してファイルを読み書きしていたのと同様に、ofstreamのインスタンスを生成しこれを通してファイルに書き込みます。

ofstreamのコンストラクタにはファイルのパス文字列(ファイルの場所)を指定します。
これはC言語形式の文字列(char型の配列、ポインタ)でもstringクラスでも構いません。
fopen関数は引数に読み取りモードか書き込みモードかを指定していましたが、ofstreamは出力専用であるのでオープンモードの指定は必要ありません。

オープン成否のチェック

ofstreamのインスタンスを生成したら、ファイルのオープンに成功したかどうかをチェックします。
これはインスタンスをif文などの条件式にセットすることで判別できます。
成功していれば真となり、そのままif文が実行され、失敗していれば偽となりif文は実行されません。
!演算子(論理否定演算子)で結果を反転させることもできます。

if (ofs)
{
	//オープン成功
}
if (!ofs)
{
	//オープン失敗
}

ファイルのオープンに成功したら、書き込みたい文字列を指定します。
これはコンソール出力(std::cout)と全く同一に行えます。

ファイルのクローズは明示的に指定する必要はありません。
ostreamのデストラクタ内でファイルを閉じる処理が自動的に行われるからです。
fopen/fclose関数に比べてファイルの閉じ忘れがなく安全です。

ostreamのcloseメンバ関数で明示的に閉じることもできます。
同様に、openメンバ関数も存在します。

std::ofstream ofs;
ofs.open(fileName);
ofs << "abc" << std::endl;
ofs.close();

これらを使用すると、別のファイルの読み書きにインスタンスを使いまわすことができます。

ファイルオープンモード

fopen関数と同様に、ofstreamにもファイルのオープンモードが存在します。

std::ios::out
書き込みモード(デフォルト)
書き込み時に、以前の内容は破棄される
std::ios::in
読み取りモード
std::ios::app
追記モード
常にファイルの末尾に書き込み
(append)
std::ios::ate
オープンと同時にファイル末尾に移動
任意の位置に移動可能
std::ios::inと同時に指定する(後述)
(at end)
std::ios::trunc
上書きモード
ファイルを開いた時点で以前の内容が破棄される
(truncate)
std::ios::binary
バイナリモード

これらはstd::ios名前空間に存在します。
(「std::ios::app」など)
ファイルの読み書き動作はfopen関数も参考にしてください。

ofstreamは書き込み専用であるため、どのようなモードを指定しても書き込みが可能です。

ostreamにおいては、outとtruncは同じ動作となります。
appとateも似ていますが、動作は異なります。
appは常にファイル末尾に追記するモードですが、ateはファイル位置移動関数で位置を自由に変更することができます。

ateは単独指定では期待通りの動作にならないのでinと組み合わせて使用します。
これについては後述します。

ファイル位置変更(シーク)

ファイルの現在の位置の変更(シーク)にはseekpメンバ関数を使用します。
これはC言語でのfseek関数と同様の働きをします。

std::ios::beg
ファイルの先頭
SEEK_SET(fseek関数)と同等
std::ios::cur
ファイルの現在位置
SEEK_CURと同等
std::ios::end
ファイルの末尾
SEEK_ENDと同等

ファイルの現在位置の取得にはtellpメンバ関数があります。
これはC言語のftell関数に対応します。

サンプルコード

ファイルのオープンモードとseekp関数、tellp関数を使ったサンプルコードです。

#include <iostream>
#include <fstream>

int main()
{
    const char *fileName = "C:\\test.txt";

    //ofstreamを読み取りモードで開き、末尾に移動
    std::ofstream ofs(fileName, std::ios::in | std::ios::ate);
    if (!ofs)
    {
        std::cout << "ファイルが開けませんでした。" << std::endl;
        std::cin.get();
        return 0;
    }

    //現在のファイル位置を表示
    std::cout << "現在のファイル位置: " << ofs.tellp() << std::endl;

    ofs << "abcde" << std::endl;
    std::cout << "現在のファイル位置: " << ofs.tellp() << std::endl;

    //現在の位置から2文字分戻る
    ofs.seekp(-2, std::ios::cur);

    std::cout << "現在のファイル位置: " << ofs.tellp() << std::endl;
    ofs << "abcde" << std::endl;

    std::cin.get();
}
現在のファイル位置: 0
現在のファイル位置: 7
現在のファイル位置: 5

「C:\test.txt」を開くと「abcdeabcde」という文字列が書き込まれています。
(Windowsの場合)

ファイルへの追記書き込みなので、「C:\test.txt」に以前の内容が残っている場合は実行結果が異なります。

ファイルのオープンモードは「|」を使用して複数同時に指定可能です。
これはC言語のビット演算で使用した演算子(OR演算子)と同様です。
(内部的にはビットフラグです)

ateはファイルのオープンと同時に末尾に移動するモードですが、ostreamは書き込み専用のファイルストリームなので、単独で指定してもファイルの末尾が検出できないようです。
そのため、inと同時に指定することでファイルの末尾まで位置を移動させます。
(appは単独指定でも動作します)
ただし、ostreamにはファイル読み取り関数がないので、inを指定してもファイル内容を読み取って表示させるようなことはできません。

24行目では、現在のファイル位置から2文字分前に戻しています。
Windows環境では改行文字は2文字ですから、21行目の最後の「std::endl」による改行文字分後ろに戻ることになります。
そこから再度「abcde」と書き込むので、結果として「abcdeabcde」という文字列がファイルに書き込まれることになります。

ファイルをate(at end)モードで開いているので、このコードを実行する度にファイルに同じ内容が追記されていくことを確認してください。

入力ストリーム

ファイルの読み取りにはifstreamを使用します。

#include <iostream>
#include <fstream>
#include <string>

int main()
{
    const char *fileName = "C:\\test.txt";

    {
        std::ofstream ofs(fileName);
        if (!ofs)
        {
            std::cout << "ファイルが開けませんでした。" << std::endl;
            std::cin.get();
            return 0;
        }
        ofs << "This is a pen." << std::endl;
    }//ofsの寿命はここまで
    //ファイルが閉じられ、外部からも読み取り可能になる

    //↑で作ったファイルを読み取り専用で開く
    std::ifstream ifs(fileName);
    if (!ifs)
    {
        std::cout << "ファイルが開けませんでした。" << std::endl;
        std::cin.get();
        return 0;
    }
    std::string data;

    //ファイルからstringにデータ格納
    ifs >> data;

    std::cout << data << std::endl;

    std::cin.get();
}
This

ifstreamも、使い方はstd::cinと同様です。
実行結果を見ると、「This」までしか取得できていません。
これもstd::cinと同様で、空白文字を区切り文字と判断します。

改行までを読み込む

改行文字までを読み込みたい場合はstd::getline関数を使用します。

std::string data;
std::getline(ifs, data);

ifstreamにもgetline関数は存在します。
(「ifs.getline(data)」で使用可能)
しかしこの形式は引数にchar*型しか受け付けてくれず、stringクラスのインスタンスに直接値を格納することができません。
C++ではstring型を使った方が便利で安全なので、特別な理由がない限りはstd名前空間のgetline関数を使用します。

ファイル終端チェック

eof関数は、ファイルが終端に達した場合に真を返します。
これを利用してファイルの内容をすべて読み込むサンプルです。
(「C:\test.txt」にはあらかじめ何か適当な複数行の文字を書き込んでおいてください)

#include <iostream>
#include <fstream>
#include <string>

int main()
{
    const char *fileName = "C:\\test.txt";

    std::string data;
    {
        std::ifstream ifs(fileName);
        if (!ifs)
        {
            std::cout << "ファイルが開けませんでした。" << std::endl;
            std::cin.get();
            return 0;
        }
        
        std::string buf;
        while (!ifs.eof())
        {
            std::getline(ifs, buf);
            data += buf + "\n";
        }

    }
    std::cout << data;

    std::cin.get();
}

eof関数が偽を返すまで、while文でファイルの読み取りを繰り返します。
getline関数は改行文字までを読み取りますが、改行文字自体は読み取らないので、読み取った文字列の末尾に改行を足しておきます。
この方法では、ファイルの終端が改行で終わらない場合、厳密にはファイルの内容と同一にはなりません。

入出力ストリーム

fstreamは、入力と出力を同時に行うことができます。
これらはofstreamifstreamを合体させたようなもので、使い方も同じです。
双方向ストリームとも言います。

#include <iostream>
#include <fstream>
#include <string>

int main()
{
    const char *fileName = "C:\\test.txt";

    //テスト用のファイル生成
    {
        std::ofstream ofs(fileName);
        if (!ofs)
        {
            std::cout << "ファイルが開けませんでした。" << std::endl;
            std::cin.get();
            return 0;
        }

        ofs << "This is a pen.\n";
        ofs << "That is a book.\n";
        ofs << "This is a car.\n";
        ofs << "That is a pen.\n";
    }//この時点でファイル生成完了

    std::string data;
    {
        std::fstream fs(fileName);
        if (!fs)
        {
            std::cout << "ファイルが開けませんでした。" << std::endl;
            std::cin.get();
            return 0;
        }
        
        std::string buf;
        std::string::size_type match;
        while (1)
        {
            std::getline(fs, buf);
            if (fs.eof()) //ファイル終端判定
                break;

            match = buf.find("pen");
            if (match != std::string::npos)
                buf.replace(match, 3, "drink"); //3は"pen"の文字数
            data += buf + "\n";
        }
        
        fs.clear();              //eofフラグをクリア
        fs.seekp(std::ios::beg); //ファイル位置を先頭にセット

        fs << data; //書き込み
    }
    std::cout << data;

    std::cin.get();
}

fstreamは、ファイルが存在しない場合にはエラーとなります。
そのため、今回はあらかじめofstremでテスト用のファイルを作っておきます。

このサンプルコードでは、文字列中に「pen」という文字が登場したら「drink」に置き換える処理をしています。
stringクラスのfind関数は文字列が登場する位置を返します。
見つからなかった場合はstd::string::nposを返しますので、それ以外の場合に置き換えを行います。
(C++の文字列2参照)

ifstreamの時と同じくwhile文でファイル終端(EOF)まで読み取りますが、EOF判定をwhileブロック中に移しています。
ファイルの終端に達したかどうかは読み取り関数で読み取った後でないと判定ができないため、while文の条件判定でEOF判定を行うと、一回余計に繰り返し処理が行われてしまい、ファイルの最後に余計な改行が入ってしまいます。
ファイルの読み取りの場合は大きな問題にはなりませんが、書き込みの場合、同じファイルへの書き込みを繰り返すと終端に改行がどんどん追加されてしまいます。
それを避けるために、データを読み取った後にファイル終端をチェックし、終端に達したら無限ループを抜けるようにしています。

一度ファイル終端まで達すると、seekpなどのファイル位置移動関数が効かなくなります。
clear関数を使用するとEOFフラグをリセットすることができます。
その上で、ファイル位置を先頭に戻してからファイルを上書きしています。

バイナリデータ

fstreamでバイナリを扱うには、オープンモードにstd::ios::binaryを指定します。

シフト演算子(<<、>>)によるデータの流し込みはそれに対応しているデータ型やクラスにしか使えないので、read関数write関数を使用します。

#include <iostream>
#include <fstream>
#include <string>
#include <vector>

int main()
{
    const char *fileName = "C:\\test.bin";

    int size;
    {
        std::vector<int> vec_out;
        for (int i = 0; i < 5; i++)
            vec_out.push_back(i * 2);

        size = vec_out.size();

        //ファイル書き込み
        std::ofstream ofs(fileName, std::ios::binary);
        if (!ofs)
        {
            std::cout << "ファイルが開けませんでした。" << std::endl;
            std::cin.get();
            return 0;
        }
        ofs.write(reinterpret_cast<char*>(&vec_out[0]), sizeof(int) * size);
    }

    std::vector<int> vec_in;
    vec_in.resize(size); //容量確保

    //ファイルからデータ復元
    {
        std::ifstream ifs(fileName, std::ios::binary);
        if (!ifs)
        {
            std::cout << "ファイルが開けませんでした。" << std::endl;
            std::cin.get();
            return 0;
        }
        ifs.read(reinterpret_cast<char*>(&vec_in[0]), sizeof(int) * size);
    }

    for (int i = 0; i < vec_in.size(); i++)
        std::cout << vec_in[i] << std::endl;

    std::cin.get();
}
0
2
4
6
8

一度データをファイルに保存し、ファイルからデータが復元できていることを確認するサンプルコードです。

read関数もwrite関数も、char*型(const char*型)しか扱うことができないので、それ以外のデータ型の場合はキャストが必要です。

今回はvectorのインスタンスをそのままファイルに書き出す方法を採っていますが、コンテナクラスの場合はストリームイテレータというものを使ったほうがスマートに書くことができます。