ファイルストリームの基礎 | Programming Place Plus C++編【言語解説】 第6章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 と更新されており、今後も 3年ごとに更新されます。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要

この章の概要です。


関連する話題が、以下のページにあります。

ファイルストリーム

前章で標準入出力ストリームを、第4章では文字列ストリームに触れました。この章では、ファイルの入出力を行うファイルストリームを取り上げます。

この章で取り上げることは、C言語で fopen関数でファイルを開き、何種類かある読み書き用の関数を使い、最後に fclose関数で閉じるという処理に代わるものです。C++ のファイルストリームは、標準入出力ストリームや文字列ストリームと似た使い方ができるようになっています。

出力ファイルストリーム

ファイルへデータを書き出すためには、std::ofstream を使用します。std::ofstream を使用するには、<fstream> をインクルードする必要があります。

まずは試しに、“Hello, World” という文字列をファイルへ書き出してみます。

#include <cstdlib>
#include <fstream>
#include <iostream>

int main()
{
    std::ofstream ofs("hello.txt");
    if (!ofs) {
        std::cerr << "ファイルオープンに失敗" << std::endl;
        std::exit(1);
    }

    ofs << "Hello, World" << std::endl;
}

実行結果:

実行結果(hello.txt):

Hello, World

std::ofstream型の変数を定義する際にファイルの名前を指定します。すると、その名前のファイルがオープンされます(無ければ作成されます)。fopen関数でいうところの、

fopen("hello.txt", "w");

に当たりますが、fopen関数の第2引数のように出力用であることを明示する必要がありません。なぜなら、std::ofstream である時点で、出力用であることが決まっているからです(名前の先頭の「o」が output のことで、次の「f」が file のことです)。

実際には、std::ofstream の定義時に2つ目の引数を与えることが可能で、そこに fopen関数の第2引数のようなオープンモードを指定できます。しかし多くの場合、省略した場合のデフォルトの挙動で十分です。詳細は、【標準ライブラリ】第28章を参照してください。

ファイルオープンが成功したかどうかは、std::ofstream型の変数を直接 if文で調べれば分かります。この部分のコードを抜き出すと、次のようになっています。

if (!ofs) {
}

ファイルオープンのタイミングだけでなく、ストリームの操作中に何らかのエラーが起きていないかどうかを、!演算子で問い合わせることができます。エラーが起きていたら、この結果は真です。

逆に、!演算子を使わずに問い合わせることで、エラーが起きていないことを確認できます。

if (ofs) {
    // エラーは起きていない
}

ファイルへ文字列を書き出す方法は、標準入出力ストリームや文字列ストリームと同じで、<<演算子が使えます。

ファイルをクローズしている箇所が見当たりませんが、std::ofstream型の変数の生存期間が終われば、自動的にクローズされます。つまり、変数ofs は main関数のローカル変数として宣言されているので、main関数を抜け出すときに生存期間を終えて、自動的にファイルクローズが行われます。

このような仕組みになっているため、ファイルクローズを忘れることは、ほとんどあり得ません。この仕組みは、C++ では非常に一般的であると同時に、非常に重要な考え方になっています。

この仕組みはデストラクタといいます。第13章で解説します。

生存期間を終える前に明示的にファイルをクローズしたければ、close関数を使用します。

ofs.close();

入力ファイルストリーム

ファイルからデータを読み取るためには、std::ifstream を使用します。std::ifstream を使用するには、<fstream> をインクルードする必要があります。

先ほどの「出力ファイルストリーム」のところで書き出したファイルを読み込んでみましょう。

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

int main()
{
    std::ifstream ifs("hello.txt");
    if (!ifs) {
        std::cerr << "ファイルオープンに失敗" << std::endl;
        std::exit(1);
    }

    std::string buf;
    ifs >> buf;
    if (!ifs) {
        std::cerr << "読み込みに失敗" << std::endl;
        std::exit(1);
    }

    std::cout << buf << std::endl;
}

実行結果:

Hello,

std::ifstream の場合も、変数定義の際にファイル名を指定することによって、その名前でファイルがオープンできます。

std::ifstream は入力用に開くので、fopen関数の第2引数に “r” を指定した場合と同じで、指定したファイルが存在しなければエラーになります。

読み込みの際には、標準入力ストリーム(cin) や文字列ストリーム(istringstream) と同じく、>>演算子が使えます。しかし、今回のサンプルプログラムの実行結果にあるように、想定した「Hello, World」ではなく、「Hello,」となってしまいます。

>>演算子による読み込みは、空白文字が現れるところで停止します。ですから “Hello, World” を読み取ろうとすると、1度目に “Hello,” までが得られ、2度目で “World” を得られます。

この挙動は std::noskipwsマニピュレータ(【標準ライブラリ】第30章)によって変更でき、空白文字も読み取れます。

読み込みが失敗していないかどうかは、オープンの失敗を調べるときと同様に、「if (ifs)」や「if (!ifs)」を使って行えます。あるいは、>>演算子の結果を調べても構いません。

if (ifs >> buf) {
    // 成功
}
else {
    // 失敗
}

std::getline関数を使う

1行に1つのデータが書かれているような場合、行単位で操作する方が自然で簡単です。この目的のためなら、std::getline関数を使うと簡単です。

std::getline関数は、ファイルストリームの機能ではなく、<string> で宣言されている関数です。詳細は、【標準ライブラリ】第2章で取り上げています。

std::getline関数を使うと、以下のようになります。

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

int main()
{
    std::ifstream ifs("hello.txt");
    if (!ifs) {
        std::cerr << "ファイルオープンに失敗" << std::endl;
        std::exit(1);
    }

    std::string buf;
    std::getline(ifs, buf);
    if (!ifs) {
        std::cerr << "読み込みに失敗" << std::endl;
        std::exit(1);
    }

    std::cout << buf << std::endl;
}

実行結果:

Hello, World

std::getline関数は、第1引数に入力ストリームを指定し、第2引数に受取り用の std::basic_string の変数を指定します。

受け取った1行分の文字列が、第2引数に指定した変数に格納されます。第4章でも取り上げたように、std::string は必要なメモリを自動的に割り当てるので、バッファオーバーフローは起きません。

戻り値は、ストリーム自身です。そのため、エラーチェックを組み合わせることも可能です。

if (std::getline(ifs, buf)) {
    // 成功
}
else {
    // 失敗
}

std::getline関数は、改行文字を行の区切りとみなします。改行文字そのものは読み取りません。

これはデフォルトの挙動であり、別の区切り文字を指定することが可能です。そのためには、第3引数に、区切り用に用いる文字を指定します。

std::getline関数は、ファイルストリーム専用の関数ではありません。むしろ、std::basic_string の方に関わりが深い関数であって、標準入力や文字列の入力の際に使うこともできます。その際には、第1引数を std::cin だとか、std::istringstream の変数に変えればよいです。

ファイルの終わりの検出

テキストファイルの中身を1文字ずつ読み込みつつ、それを標準出力へ書き出していくようなプログラムを書きたいとします。そのためには、ファイルの終端を検出できないといけません。

どのタイプのストリームであっても、ファイルの終端に達したかどうかは eof関数によって調べられます。C言語の feof関数に相当するものと考えればよいです。

終端に達しているかどうかは、エラーチェックで行うように「if (ifs)」といった方法でも確認できます。この場合、終端に達していれば偽になります。この方法は、エラーが起きているのか、終端に達しているのかを区別できません。

次のようなテキストファイル (test.txt) があるとして、その内容を出力するとしましょう。

Hello, World
Hello, C++

まずは、eof関数を使ったサンプルプログラムを挙げます。

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

int main()
{
    std::ifstream ifs("test.txt");
    if (!ifs) {
        std::cerr << "ファイルオープンに失敗" << std::endl;
        std::exit(1);
    }

    while (!ifs.eof()) {
        char c;
        ifs.get(c);
        std::cout << c;
    }
    std::cout << std::flush;
}

実行結果:

Hello, World
Hello, C++

get関数は1文字を読み込んで、実引数に指定した変数に格納します。戻り値は、ストリーム自身です。

【上級】正確には、戻り値はストリーム自身への参照(第16章)です。

それらしい実行結果を得られているようにも見えますが、末尾に余計な改行が入っています。これは、eof関数を呼び出すタイミングが悪いため、whileループ内の処理が1回多く回ってしまっているためです。C言語の feof関数でもそうですが、入出力の関数を呼び出した「後で」、終端判定をしないといけません(C言語編第40章参照)。

これを踏まえて、while文のところを書き直すとすれば、次のようになります。

    while (1) {
        char c;
        ifs.get(c);
        if (ifs.eof()) {
            break;
        }
        std::cout << c;
    }

しかし、これは不格好ですし、コード量が多くなって分かりづらくもあります。

そこで、eof関数を使うのではなく、エラーチェックのときと同じ方法を取ると良いです。get関数の戻り値が、ストリーム自身であることを利用できます。

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

int main()
{
    std::ifstream ifs("test.txt");
    if (!ifs) {
        std::cerr << "ファイルオープンに失敗" << std::endl;
        std::exit(1);
    }

    char c;
    while (ifs.get(c)) {
        std::cout << c;
    }
    if (!ifs.eof()) {
        std::cerr << "読み込みに失敗" << std::endl;
        std::exit(1);
    }
    std::cout << std::flush;
}

実行結果:

Hello, World
Hello, C++

エラーが発生しているのか、正常に終端まで読み込めたのかを区別しなければならないのなら、while文を抜けた後で eof関数を使ってチェックできます。


バイナリモード

バイナリファイルを扱いたいときは、std::ofstream、std::ifstream、std::fstream の変数定義時に、第2引数に std::ios_base::binary という指定を与えます。

std::ofstream ofs("test.bin", std::ios_base::out | std::ios_base::binary);
std::ifstream ofs("test.bin", std::ios_base::in | std::ios_base::binary);
std::fstream ofs("test.bin", std::ios_base::in | std::ios_base::out | std::ios_base::binary);

std::ios_base::instd::ios_base::out も登場していますが、これらはデフォルトで指定されているものです。デフォルトの定義をそのまま活かすため、|演算子で結合して渡しています。

実際に、バイナリデータの入出力を試してみましょう。

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

int main()
{
    std::ofstream ofs("test.bin", std::ios_base::out | std::ios_base::binary);
    if (!ofs) {
        std::cerr << "ファイルオープンに失敗" << std::endl;
        std::exit(1);
    }

    int num = 900;
    double d = 7.85;
    std::string str("xyzxyz");
    std::string::size_type strLen = str.size();

    ofs.write((const char*)&num, sizeof(num));
    ofs.write((const char*)&d, sizeof(d));
    ofs.write((const char*)&strLen, sizeof(strLen));
    ofs.write(str.c_str(), strLen);
    ofs.close();


    std::ifstream ifs("test.bin", std::ios_base::in | std::ios_base::binary);
    if (!ifs) {
        std::cerr << "ファイルオープンに失敗" << std::endl;
        std::exit(1);
    }

    ifs.read((char*)&num, sizeof(num));
    ifs.read((char*)&d, sizeof(d));
    ifs.read((char*)&strLen, sizeof(strLen));
    std::string s(strLen, '\0');
    ifs.read(&s[0], strLen);
    if (!ifs) {
        std::cerr << "読み込みに失敗" << std::endl;
        std::exit(1);
    }

    std::cout << num << "\n"
              << d << "\n"
              << s << std::endl;
}

実行結果:

900
7.85
xyzxyz

やや複雑ですが、std::ofstream を使ってバイナリデータを書き出し、書き出されたファイルを std::ifstream で読み取って、標準出力に出力することで確認しています。

バイナリデータを書き出すには、write関数を使います。この関数の第1引数が書き出すデータのメモリアドレスで、第2引数が大きさになっています。

第1引数の型は、const char*型です。そのため、煩わしいことに、他の型を書き出す際にはキャストが必要です。

ここではC言語のキャスト構文を使っていますが、C++ では static_cast を使うようにしましょう(第7章)。

一方、読み込みの際には、read関数を使います。こちらは、第1引数が受け取り先のメモリアドレス、第2引数が大きさです。第1引数の型は、char*型になるので、やはり他の型を読み込む際にはキャストが必要です。

このサンプルプログラムでは文字列を std::string で扱っています。この文字列を write関数に渡す際には、c_str関数(【標準ライブラリ】第2章)を使って const char*型のポインタを受け取り、これを渡せばよいです。

一方、read関数で文字列を受け取る際には、std::string の内部にある配列のメモリアドレスを指定してやる必要があります。そのためには、std::string が内部に十分な大きさの配列を確保していなければなりません。そこで、次のように変数定義を行っています。

std::string s(strLen, '\0');

変数定義時に指定できる引数の意味については、【標準ライブラリ】第2章で取り上げています。

第1引数に文字数(count)、第2引数に文字(c) を指定しています。すると、c を count個持った文字列で初期化されます。strLen が 6 であれば ‘\0’ が6個、つまり “\0\0\0\0\0\0” で初期化されます。これで6文字分の領域が確保できました。


練習問題

問題① 標準入力から受け取った内容を、テキストファイルへ書き出すプログラムを作成してください。細かい仕様はお任せします。

問題② 既存のテキストファイルのコピーを作り出すプログラムを作成してください。


解答ページはこちら

参考リンク


更新履歴

’2018/8/11 全体的に見直し修正。
– std::getline関数の話題を、「std::getline関数を使う」の項に分離。
–「入出力両用のファイルストリーム」の項を削除。【標準ライブラリ】第28章に任せる。
– エラーチェックの方法について補足と修正。

’2018/7/12 「バイナリモード」の項のサンプルプログラムを修正。
– 読み込み時に char型の配列を使わず、std::string で行うようにした。

’2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

’2013/12/23 新規作成。



前の章へ (第5章 標準入出力ストリームの基礎)

次の章へ (第7章 C++ の型とキャスト)

C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る