ファイルストリーム

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

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

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

出力ストリーム

ファイルの書き込みにはstd::ofstreamクラスを使用します。
これを使用するには#include <fstream>が必要です。


#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;

    //ファイルを閉じる
    ofs.close();

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

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

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

パスの指定は相対パスでも可能です。
その場合、実行ファイルの場所が起点となります。
(Visual Studioのプロジェクトフォルダ内)

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

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

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

open関数

ofstreamのコンストラクタを呼び出す以外にも、インスタンスからopen関数を実行することでもファイルを開くことができます。


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

使い方はコンストラクタの場合と同じです。

オープン成否のチェック

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


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

ファイルのクローズ

ファイルのクローズはofstreamcloseメンバ関数で行います。

また、ofstreamクラスのデストラクタではファイルのクローズ処理が行われるため、インスタンスの寿命が尽きると自動的にクローズされます。
fopen/fclose関数に比べてファイルの閉じ忘れがなく安全です。

ファイルオープンモード

fopen関数と同様に、ofstreamにもファイルのオープンモードが存在します。
以下の値の組み合わせをコンストラクタ(またはopen関数)の第二引数に指定します。

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

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

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

appateは似ていますが異なります。
appはファイル位置を末尾より手前に移動させても書き込みは常に末尾に行われます。
ateはファイル位置を移動させた位置に書き込みが可能です。

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

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

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

第一引数はファイル位置のオフセット(第二引数からの相対位置)です。
第二引数は以下の値のいずれかを指定します。

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

ファイル位置取得

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

この関数に引数はなく、戻り値はstd::streampos型です。
これはストリーム上の位置を表す値です。
内部的にはクラスで実装されていますが、std::streampos型同士を+や-で演算したり、std::coutで数値を出力したりできます。

サンプルコード

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


#include <iostream>
#include <fstream>

//現在のファイル位置を表示する関数
std::streampos tellFilePoint(std::ofstream& ofs)
{
    std::streampos pos = ofs.tellp();
    std::cout << "現在のファイル位置: " << pos << std::endl;
    return pos;
}

int main()
{
    const char* fileName = "C:\\test.txt";
    {
        //ファイルを新規に作成
        std::ofstream ofs(fileName);
        if (!ofs) {
            std::cout << "ファイルが開けませんでした。" << std::endl;
            std::cin.get();
            return 0;
        }

        //適当に書き込んでファイルを閉じる
        ofs << "abc";

        //ofsの寿命でファイルはクローズされるので
        //↓は無くても同じ
        ofs.close();
    }
    {
        //上で作ったファイルを開いて終端に移動
        std::ofstream ofs(fileName, std::ios::in | std::ios::ate);
        if (!ofs)
        {
            std::cout << "ファイルが開けませんでした。" << std::endl;
            std::cin.get();
            return 0;
        }

        //現在のファイル位置を取得&表示
        std::streampos pos = tellFilePoint(ofs);

        ofs << "12345";
        tellFilePoint(ofs);

        //ファイルを開いた当初の位置に戻す
        ofs.seekp(pos, std::ios::beg);
        tellFilePoint(ofs);

        ofs << "678";
        tellFilePoint(ofs);

        ofs.close();
    }

    std::cin.get();
}
現在のファイル位置: 3
現在のファイル位置: 8
現在のファイル位置: 3
現在のファイル位置: 6

上記コードを実行すると「C:\test.txt」にファイルが作成され、「abc67845」という文字列が書き込まれます。

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

入力ストリーム

ファイルの読み取りにはstd::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::cin.get();
}
This

ifstreamも、使い方はstd::cinと同様に>>で変数に格納できます。
実行結果を見るとファイルの内容である「This is a pen.」のうち「This」までしか取得できていませんが、これもstd::cinと同様で空白文字が入力の区切り文字と判断されるからです。

改行までを読み込む

改行文字までを読み込みたい場合はstd::getline関数を使用します。
この関数は<string>ヘッダファイルのインクルードが必要です。

template <class CharType, class Traits, class Allocator>
basic_istream<CharType, Traits>& getline(
 basic_istream<CharType, Traits>& in_stream,
 basic_string<CharType, Traits, Allocator>& str,
 CharType delimiter);
入力ストリームin_streamから文字delimiterが出現するまでを読み取り、stringインスタンスstrに格納する。
戻り値は入力ストリームへの参照。
template <class CharType, class Traits, class Allocator>
basic_istream<CharType, Traits>& getline(
 basic_istream<CharType, Traits>& in_stream,
 basic_string<CharType, Traits, Allocator>& str);
入力ストリームin_streamから改行が出現するまでを読み取り、stringインスタンスstrに格納する。
戻り値は入力ストリームへの参照。

std::string data;
std::ifstream ifs("a.txt");
std::getline(ifs, data); //一行読み取る

第一引数は読み取りを行うストリームを指定します。
この関数はC言語のfgets関数と同じように、入力ストリームを指定して文字列を取得できます。
ここにstd::cinを指定すると標準入力から文字列を読み取ります。

第二引数には読み取った文字列を格納するstringクラスのインスタンスを指定します。

第三引数は読み取りの区切りとなる文字を指定します。
この引数は省略可能で、省略した場合は改行文字までを読み取ります。

std::getline関数はstringクラスの関連関数として定義されています。
(<string>ヘッダ内に宣言がある)
ifstreamにもgetlineメンバ関数が存在しますが、こちらは格納先にstringクラスを指定できず、char型の配列を指定する必要があります。
C++ではstring型を使った方が便利で安全なので、特別な理由がない限りはstd::getline関数を使用したほうが良いでしょう。

ファイル終端チェック

eof関数

eof関数は、ファイルが終端に達した場合に真を返します。
これを利用してファイルの内容をすべて読み込むサンプルです。


#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;
    }

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

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

        std::string buf;
        std::getline(ifs, buf);
        data += buf;
        while (!ifs.eof())
        {
            data += "\n";
            std::getline(ifs, buf);
        }

    }
    std::cout << data;

    std::cin.get();
}

eof関数が偽を返すまで、while文でファイルの読み取りを繰り返します。
getline関数は改行文字までを読み取りますが、改行文字自体は変数に格納しないので、読み取った文字列の末尾に改行を足しておきます。

ストリームの状態を返す関数は以下があります。
(文字の入出力#入力のエラーチェックで紹介したものと同じです)

good()
通常状態(エラーなし)
eof()
ファイルの終端
fail()
読み取りの失敗
bad()
その他のエラー

ストリーム自体をチェック

ストリーム自身を条件判定すると、fail状態またはbad状態のときに偽を返します。
これを利用してファイル終端を検出することもできます。
(正確には、読み取りエラー発生の判定)
>>演算子による入力やgetline関数などは戻り値としてストリーム自身を返すので、以下のような記述が可能です。


std::ifstream ifs(fileName);
std::string buf;
//真を返す場合は読み取り成功
while (std::getline(ifs, buf))
{
	data += buf + "\n";
}

状態のクリア

eofなどの状態になったストリームを通常状態(good)に戻すにはclear関数を使用します。


std::ifstream ifs(fileName);
std::string buf;
//真を返す場合は読み取り成功
while (std::getline(ifs, buf))
{
	data += buf + "\n";
}

//ここに到達した時点でストリームはgood以外の状態

//状態をgoodに戻す
ifs.clear();
//ファイル位置を先頭に戻す
ifs.seekg(0, std::ios::beg);

一度good以外の状態になったストリームに対してはこの関数でgood状態に戻しておかないと、シークなども行えなくなるので注意してください。

ファイル位置の設定と取得

入力ストリームでファイル位置を変更する場合はseekg関数を使用します。
現在のファイル位置はtellg関数で取得できます。

出力ストリームの場合はseekp、tellp、入力ストリームの場合はseekg、tellgと微妙に名前が異なります。
(put(書き込み)のp、get(読み取り)のgだそうです)

読み取り用も書き込み用も動作は同じです。

入出力ストリーム

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


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

int main()
{
    const char* fileName = "R:\\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;
        size_t match;
        while (std::getline(fs, buf))
        {
            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();
}
This is a drink.
That is a book.
This is a car.
That is a drink.

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

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

バイナリデータ

ファイルストリームでバイナリを扱うには、オープンモードにstd::ios::binaryを指定します。

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

basic_istream<Char_T, Tr>& read(
 char_type* str,
 streamsize count);
ストリームに文字列strをcount文字分を書き込む。
戻り値はストリームへの参照。
basic_ostream<Elem, Tr>& write(
 const char_type* str,
 streamsize count);
ストリームから文字列strにcount文字分を読み取る。
戻り値はストリームへの参照。

1バイトデータはchar型で扱うので「文字」や「文字列」となっていますが、それ以外のデータも扱うことができます。
C言語的な文字列ではないので、読み取ったデータの終端にNULL文字は付けられません。

streamsizeというデータ型はストリームのサイズを表す符号つき整数型です。
(std名前空間)


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

int main()
{
    auto a = sizeof(0);
    const char* fileName = "R:\\test.bin";

    //書き込むデータのサイズ(要素数)
    size_t length;

    //ファイルに書き込み
    {
        //書き込むデータの作成
        std::vector<int> vec_out;
        for (int i = 0; i < 5; i++)
            vec_out.push_back(i * 2);

        length = 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*>(&length), sizeof(size_t));
        ofs.write(reinterpret_cast<char*>(&vec_out[0]), sizeof(int) * static_cast<std::streamsize>(length));
    }

    //データ受け取り用
    std::vector<int> vec_in;

    //ファイルからデータ復元
    {
        std::ifstream ifs(fileName, std::ios::binary);
        if (!ifs)
        {
            std::cout << "ファイルが開けませんでした。" << std::endl;
            std::cin.get();
            return 0;
        }

        //ファイル先頭から要素数を読み取り容量確保
        ifs.read(reinterpret_cast<char*>(&length), sizeof(size_t));
        vec_in.resize(length);
        ifs.read(reinterpret_cast<char*>(&vec_in[0]), sizeof(int) * static_cast<std::streamsize>(length));
    }

    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*型)しか扱うことができないので、それ以外のデータ型の場合はキャストが必要です。

int型のvectorの要素をファイルに書き込むのですが、ファイルの読み込み時にも要素数が分かるようにファイルの先頭にsize_t型のサイズで要素数を書き込んでいます。
今回はそういう決まりのファイルを作成していますが、このフォーマットを知らない他のプログラムからはこの書き出したファイルを正確に読み取ることは出来ません。

なお、vectorクラスの各要素は配列のように連続したメモリ領域に確保されることが保証されていますが、このような保証がないクラスではアドレスを指定してデータを一気に読み書きすることはできないので注意してください。
実際のプログラムではこのような読み書きするようなことはほぼなく、専用のクラスを作ることになるでしょう。