C++でのファイル入出力について

AI実装検定のご案内

C++では、標準ライブラリの<fstream>を使用して、ファイルの読み込みや書き込みを行います。

主に使用するクラスは、次の3つです。

クラス主な用途
std::ifstreamファイルからデータを読み込む
std::ofstreamファイルへデータを書き込む
std::fstreamファイルの読み込みと書き込みの両方を行う

テキストファイルだけでなく、画像や独自形式のデータなどを扱うバイナリファイルにも使用できます。

基本的には、次のヘッダーファイルを読み込みます。

#include <fstream>

画面へのメッセージ出力や文字列の処理も行う場合は、次のヘッダーも使用します。

#include <fstream>
#include <iostream>
#include <string>
目次

ファイルへデータを書き込む

std::ofstreamを使った基本的な書き込み

ファイルへデータを書き込む場合は、std::ofstreamを使用します。

#include <fstream>
#include <iostream>

int main()
{
    std::ofstream outputFile("example.txt");

    if (!outputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    outputFile << "Hello, C++!\n";
    outputFile << "ファイルへの書き込みテストです。\n";

    return 0;
}

このプログラムを実行すると、実行時のカレントディレクトリにexample.txtが作成されます。

ファイルの内容は、次のようになります。

Hello, C++!
ファイルへの書き込みテストです。

std::ofstreamでは、画面出力に使用するstd::coutと同じように、<<演算子を使ってデータを書き込めます。

outputFile << "書き込む文字列\n";

既存ファイルを開いた場合の動作

次のようにstd::ofstreamでファイルを開くと、通常は既存の内容が削除されます。

std::ofstream outputFile("example.txt");

すでにexample.txtが存在していた場合、その内容を空にしてから新しいデータが書き込まれます。

既存の内容を残したい場合は、後述する追記モードを使用します。

open()でファイルを開く

ファイルストリームを作成した後、open()を使ってファイルを開くこともできます。

#include <fstream>
#include <iostream>

int main()
{
    std::ofstream outputFile;

    outputFile.open("example.txt");

    if (!outputFile.is_open())
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    outputFile << "open関数でファイルを開きました。\n";

    outputFile.close();

    return 0;
}

is_open()を使用すると、ファイルが実際に開かれているか確認できます。

if (outputFile.is_open())
{
    // ファイルが開かれている場合の処理
}

ただし、ファイルが開かれていても、その後の書き込みに成功するとは限りません。

重要な処理では、書き込み後のストリーム状態も確認する必要があります。

既存ファイルへ追記する

std::ios::appを使用する

既存の内容を残したまま、ファイルの末尾へデータを追加したい場合は、std::ios::appを指定します。

#include <fstream>
#include <iostream>

int main()
{
    std::ofstream outputFile(
        "example.txt",
        std::ios::app
    );

    if (!outputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    outputFile << "この文章はファイル末尾に追加されます。\n";

    if (!outputFile)
    {
        std::cerr << "書き込みに失敗しました。\n";
        return 1;
    }

    return 0;
}

appは、appendの略で「追記」を意味します。

std::ios::appを指定すると、すべての書き込みがファイルの末尾で行われます。

std::ios::appstd::ios::ateの違い

std::ios::appstd::ios::ateは、どちらもファイルの末尾に関係するモードですが、動作が異なります。

std::ios::appでは、書き込みのたびにファイル末尾へ移動します。

std::ofstream file(
    "example.txt",
    std::ios::app
);

書き込み位置を変更しようとしても、実際の書き込みは末尾で行われます。

一方、std::ios::ateでは、ファイルを開いた直後に一度だけ末尾へ移動します。

std::fstream file(
    "example.txt",
    std::ios::in |
    std::ios::out |
    std::ios::ate
);

その後は、seekg()seekp()を使って別の位置へ移動できます。

ファイルからデータを読み込む

std::ifstreamを使った基本的な読み込み

ファイルからデータを読み込む場合は、std::ifstreamを使用します。

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

int main()
{
    std::ifstream inputFile("example.txt");

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    std::string line;

    while (std::getline(inputFile, line))
    {
        std::cout << line << '\n';
    }

    return 0;
}

このコードでは、ファイルを1行ずつ読み込み、画面へ表示しています。

std::getline()で1行ずつ読み込む

std::getline()は、改行までの文字列を読み込みます。

std::getline(inputFile, line);

通常は、次のように読み込み処理そのものをwhile文の条件にします。

while (std::getline(inputFile, line))
{
    std::cout << line << '\n';
}

読み込みに成功している間だけループが続くため、ファイル末尾やエラーを自然に処理できます。

>>演算子で空白区切りのデータを読み込む

単語や数値を空白区切りで読み込む場合は、>>演算子を使用します。

たとえば、data.txtの内容が次のようになっているとします。

Alice 25
Bob 32
Carol 28

次のコードで読み込めます。

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

int main()
{
    std::ifstream inputFile("data.txt");

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    std::string name;
    int age = 0;

    while (inputFile >> name >> age)
    {
        std::cout
            << "名前: " << name
            << ", 年齢: " << age
            << '\n';
    }

    return 0;
}

>>演算子は、空白、タブ、改行などを区切りとして扱います。

そのため、次のように空白を含む名前は、複数のデータに分割されます。

John Smith 30

空白を含む文字列を読み込みたい場合は、std::getline()を使用する必要があります。

>>std::getline()を組み合わせる際の注意点

改行文字が残る問題

次のように、>>による読み込みの直後にstd::getline()を使うと、意図しない空文字列が読み込まれることがあります。

int age = 0;
std::string name;

inputFile >> age;
std::getline(inputFile, name);

inputFile >> ageは数値だけを読み込み、その後ろの改行文字を入力ストリーム内に残します。

そのため、直後のstd::getline()が残っている改行だけを読み込み、空文字列を返すことがあります。

std::wsを使って空白を読み飛ばす

一般的な対処方法として、std::wsを使用できます。

inputFile >> age;
std::getline(inputFile >> std::ws, name);

具体例は次のとおりです。

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

int main()
{
    std::ifstream inputFile("person.txt");

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    int age = 0;
    std::string name;

    if (!(inputFile >> age))
    {
        std::cerr << "年齢を読み込めませんでした。\n";
        return 1;
    }

    std::getline(inputFile >> std::ws, name);

    std::cout << "年齢: " << age << '\n';
    std::cout << "名前: " << name << '\n';

    return 0;
}

ただし、std::wsは改行文字だけでなく、連続する空白やタブもすべて読み飛ばします。

行頭の空白をデータとして残したい場合には適していません。

入力形式を自由に設計できる場合は、最初からすべてstd::getline()で読み込み、必要に応じて文字列から数値へ変換する方法もあります。

数値をファイルへ保存する

テキスト形式で数値を書き込む

数値も<<演算子を使って書き込めます。

#include <fstream>
#include <iostream>

int main()
{
    std::ofstream outputFile("numbers.txt");

    if (!outputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    const int count = 10;
    const double price = 1250.5;

    outputFile << count << '\n';
    outputFile << price << '\n';

    if (!outputFile)
    {
        std::cerr << "書き込みに失敗しました。\n";
        return 1;
    }

    return 0;
}

作成されるファイルは、次のようになります。

10
1250.5

テキスト形式の数値を読み込む

読み込む場合は、対応する型の変数を用意します。

#include <fstream>
#include <iostream>

int main()
{
    std::ifstream inputFile("numbers.txt");

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    int count = 0;
    double price = 0.0;

    if (!(inputFile >> count >> price))
    {
        std::cerr << "データの読み込みに失敗しました。\n";
        return 1;
    }

    std::cout << "個数: " << count << '\n';
    std::cout << "価格: " << price << '\n';

    return 0;
}

ファイルの内容が数値として解釈できない場合、読み込みは失敗します。

外部ファイルを扱う場合は、型変換の成功だけでなく、値の範囲も検証することが重要です。

ファイルのオープンモード

主なオープンモード

ファイルを開く際には、用途に応じてオープンモードを指定できます。

モード意味
std::ios::in読み込み用として開く
std::ios::out書き込み用として開く
std::ios::appすべての書き込みを末尾で行う
std::ios::ate開いた直後に末尾へ移動する
std::ios::trunc既存内容を削除する
std::ios::binaryバイナリモードで開く

複数のモードを組み合わせる場合は、|演算子を使用します。

std::fstream file(
    "example.dat",
    std::ios::in |
    std::ios::out |
    std::ios::binary
);

C++23のstd::ios::noreplace

C++23以降では、既存ファイルを上書きせずに新規作成したい場合に、std::ios::noreplaceを使用できます。

std::ofstream file(
    "newfile.txt",
    std::ios::out |
    std::ios::noreplace
);

指定したファイルがすでに存在する場合、オープンに失敗します。

ただし、C++20以前の環境では使用できません。

std::fstreamで読み書きする

読み込みと書き込みの両方に使用する

同じファイルに対して読み込みと書き込みの両方を行う場合は、std::fstreamを使用します。

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

int main()
{
    std::fstream file(
        "example.txt",
        std::ios::in |
        std::ios::out
    );

    if (!file)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    std::string line;

    while (std::getline(file, line))
    {
        std::cout << line << '\n';
    }

    return 0;
}

std::ios::in | std::ios::outで開く場合、対象ファイルが存在しないとオープンに失敗することがあります。

新しいファイルを確実に作成したい場合は、先にstd::ofstreamで作成するなど、用途に合わせた処理が必要です。

読み込み後に書き込む場合の注意点

ファイル末尾まで読み込むと、ストリームにEOF状態が設定されます。

そのままでは書き込みや位置移動が正常に行えないことがあります。

std::fstream file(
    "example.txt",
    std::ios::in |
    std::ios::out
);

std::string line;

while (std::getline(file, line))
{
    std::cout << line << '\n';
}

file.clear();
file.seekp(0, std::ios::end);

file << "追加データ\n";

clear()でエラー状態を解除し、seekp()で書き込み位置を末尾へ移動しています。

読み込みと書き込みを切り替える場合は、目的の位置を明示的に設定した方が安全です。

ファイルを閉じる

close()による明示的なクローズ

ファイルを明示的に閉じる場合は、close()を使用します。

outputFile.close();

スコープを抜けた際の自動クローズ

ファイルストリームは、スコープを抜けると自動的に破棄されます。

その際にファイルも閉じられます。

int main()
{
    {
        std::ofstream file("example.txt");

        if (!file)
        {
            return 1;
        }

        file << "Hello\n";
    }

    // この時点でfileは破棄されている
}

これは、C++のRAIIと呼ばれる資源管理の仕組みに基づいています。

通常の処理では、必ずしもclose()を明示的に呼ぶ必要はありません。

書き込み成功を確認する場合

自動的に閉じられることと、書き込みに成功したことを確認できることは別の問題です。

重要なファイルを保存する場合は、close()後の状態も確認します。

outputFile << "重要なデータ\n";

if (!outputFile)
{
    std::cerr << "書き込みに失敗しました。\n";
    return 1;
}

outputFile.close();

if (!outputFile)
{
    std::cerr << "ファイルを正常に閉じられませんでした。\n";
    return 1;
}

デストラクターにクローズを任せるだけでは、終了時に発生したエラーを戻り値として確認できません。

ファイルストリームの状態を確認する

主な状態確認関数

ファイルストリームには、状態を確認するための関数があります。

関数意味
good()エラーがなく正常な状態
eof()ファイル末尾へ到達した
fail()読み込みや変換などに失敗した
bad()回復が難しい入出力エラーが発生した
is_open()ファイルが開かれている

fail()の使用例

#include <fstream>
#include <iostream>

int main()
{
    std::ifstream inputFile("number.txt");

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    int value = 0;
    inputFile >> value;

    if (inputFile.fail())
    {
        std::cerr << "整数の読み込みに失敗しました。\n";
        return 1;
    }

    std::cout << value << '\n';

    return 0;
}

たとえば、number.txtに次の内容が入っているとします。

abc

この文字列をint型へ読み込もうとすると変換に失敗し、fail()trueになります。

while (!eof())を使用してはいけない理由

問題のある書き方

次のようなコードは推奨されません。

while (!inputFile.eof())
{
    std::getline(inputFile, line);
    std::cout << line << '\n';
}

eof()は、次の読み込みが成功するかを事前に確認する関数ではありません。

ファイル末尾を越えて読み込もうとした後に、EOF状態が設定されます。

そのため、読み込みに失敗したデータを処理したり、最後の内容を重複して扱ったりする可能性があります。

推奨される書き方

読み込み処理そのものを条件式にします。

while (std::getline(inputFile, line))
{
    std::cout << line << '\n';
}

数値の場合も同様です。

int value = 0;

while (inputFile >> value)
{
    std::cout << value << '\n';
}

この書き方であれば、読み込みに成功したデータだけを処理できます。

ストリームのエラー状態を解除する

clear()の役割

読み込みに失敗したり、ファイル末尾へ到達したりすると、ストリームに状態フラグが設定されます。

そのままでは、後続の読み込みや位置移動が正常に行えない場合があります。

状態を解除するには、clear()を使用します。

file.clear();

ファイルを先頭から再度読み込む

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

int main()
{
    std::ifstream file("example.txt");

    if (!file)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    std::string line;

    while (std::getline(file, line))
    {
        std::cout << line << '\n';
    }

    file.clear();
    file.seekg(0, std::ios::beg);

    std::cout << "もう一度読み込みます。\n";

    while (std::getline(file, line))
    {
        std::cout << line << '\n';
    }

    return 0;
}

EOF状態をclear()で解除し、seekg()で読み込み位置を先頭へ戻しています。

ファイル内の位置を移動する

読み書き位置を操作する関数

ファイルストリームは、現在の読み込み位置や書き込み位置を保持しています。

関数用途
seekg()読み込み位置を変更する
seekp()書き込み位置を変更する
tellg()現在の読み込み位置を取得する
tellp()現在の書き込み位置を取得する

gはget、pはputを表しています。

位置指定の基準

基準意味
std::ios::begファイルの先頭
std::ios::cur現在位置
std::ios::endファイルの末尾

たとえば、読み込み位置をファイル先頭へ移動する場合は、次のようにします。

file.seekg(0, std::ios::beg);

書き込み位置を末尾へ移動する場合は、次のとおりです。

file.seekp(0, std::ios::end);

ファイルサイズを取得する

std::filesystem::file_size()を使用する

通常のファイルサイズを取得する場合は、C++17以降のstd::filesystem::file_size()が分かりやすい方法です。

#include <filesystem>
#include <iostream>
#include <system_error>

int main()
{
    const std::filesystem::path path = "example.txt";

    std::error_code error;
    const auto size =
        std::filesystem::file_size(path, error);

    if (error)
    {
        std::cerr
            << "ファイルサイズを取得できませんでした。\n";
        return 1;
    }

    std::cout
        << "ファイルサイズ: "
        << size
        << "バイト\n";

    return 0;
}

seekg()tellg()を使用する

バイナリファイルを末尾まで移動し、現在位置を取得する方法もあります。

#include <fstream>
#include <iostream>

int main()
{
    std::ifstream file(
        "example.txt",
        std::ios::binary
    );

    if (!file)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    file.seekg(0, std::ios::end);

    const std::streampos size = file.tellg();

    if (size == std::streampos(-1))
    {
        std::cerr
            << "ファイルサイズを取得できませんでした。\n";
        return 1;
    }

    std::cout
        << "ファイルサイズ: "
        << size
        << "バイト\n";

    return 0;
}

通常のローカルファイルでは使用できますが、tellg()は一般的なストリーム位置を返す関数です。

通常ファイルのサイズを取得する目的であれば、std::filesystem::file_size()の方が意図を明確に表せます。

ファイル全体を一度に読み込む

std::istreambuf_iteratorを使用する

ファイル全体をstd::stringへ読み込む方法の一つです。

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

int main()
{
    std::ifstream inputFile(
        "example.txt",
        std::ios::binary
    );

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    const std::string content{
        std::istreambuf_iterator<char>(inputFile),
        std::istreambuf_iterator<char>()
    };

    if (inputFile.bad())
    {
        std::cerr << "読み込み中にエラーが発生しました。\n";
        return 1;
    }

    std::cout << content;

    return 0;
}

std::istreambuf_iteratorを使用するため、<iterator>を明示的にインクルードします。

std::ostringstreamを使用する

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

int main()
{
    std::ifstream inputFile(
        "example.txt",
        std::ios::binary
    );

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    std::ostringstream buffer;
    buffer << inputFile.rdbuf();

    if (inputFile.bad())
    {
        std::cerr << "読み込み中にエラーが発生しました。\n";
        return 1;
    }

    const std::string content = buffer.str();

    std::cout << content;

    return 0;
}

ファイル全体を読み込む方法は簡潔ですが、ファイルサイズに応じたメモリを消費します。

巨大なファイルでは、1行ずつ、または一定サイズずつ読み込む方が安全です。

バイナリファイルを読み書きする

バイナリモードでファイルを開く

画像、音声、圧縮データ、独自形式のファイルなどをバイト列として扱う場合は、std::ios::binaryを指定します。

std::ifstream inputFile(
    "example.bin",
    std::ios::binary
);

バイナリデータの読み書きでは、主に次の関数を使用します。

関数用途
write()バイト列を書き込む
read()バイト列を読み込む
gcount()直前に読み込んだバイト数を取得する

整数をバイナリ形式で書き込む

#include <fstream>
#include <iostream>

int main()
{
    std::ofstream outputFile(
        "number.bin",
        std::ios::binary
    );

    if (!outputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    const int value = 12345;

    outputFile.write(
        reinterpret_cast<const char*>(&value),
        sizeof(value)
    );

    if (!outputFile)
    {
        std::cerr << "書き込みに失敗しました。\n";
        return 1;
    }

    return 0;
}

write()はバイト列を受け取るため、intへのポインターをconst char*へ変換しています。

整数をバイナリ形式で読み込む

#include <fstream>
#include <iostream>

int main()
{
    std::ifstream inputFile(
        "number.bin",
        std::ios::binary
    );

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    int value = 0;

    inputFile.read(
        reinterpret_cast<char*>(&value),
        sizeof(value)
    );

    if (!inputFile)
    {
        std::cerr << "読み込みに失敗しました。\n";
        return 1;
    }

    std::cout
        << "読み込んだ値: "
        << value
        << '\n';

    return 0;
}

生の整数保存は環境依存である

intのメモリ表現をそのまま保存する方法は、同じプログラムや同じ環境内での簡易的な保存には使用できます。

ただし、長期保存や異なる環境間での共有には注意が必要です。

環境によって、次の要素が異なる可能性があります。

  • intのサイズ
  • バイト順序
  • CPUの種類
  • コンパイラー
  • データ型の内部表現

移植可能なファイル形式を作る場合は、std::uint32_tなどの幅を明示した型を使い、保存するバイト順序も仕様として定めます。

ただし、std::uint32_tを使用するだけでは、バイト順序の問題までは解決しません。

構造体をバイナリ保存する際の注意点

構造体をそのまま保存する例

次のような構造体をバイト列として保存することはできます。

struct Person
{
    int age;
    double height;
};
Person person{30, 170.5};

outputFile.write(
    reinterpret_cast<const char*>(&person),
    sizeof(person)
);

しかし、この方法には多くの問題があります。

  • コンパイラーによってパディングが異なる
  • CPUによってバイト順序が異なる
  • 型のサイズが環境によって異なる
  • 構造体の変更で過去データを読めなくなる
  • 浮動小数点数の表現に依存する
  • ファイル内容が実装依存になる

オブジェクト表現をバイト列として扱える可能性があるのは、基本的にトリビアルコピー可能な型です。

それでも、永続的で移植可能なファイル形式としては適していません。

std::stringを含む構造体を直接保存しない

次のような構造体を、そのままバイナリ保存してはいけません。

struct Person
{
    std::string name;
    int age;
};

std::stringの内部には、文字列データそのものだけでなく、動的メモリを管理するための情報が含まれています。

次のようにオブジェクト全体を書き込んでも、次回の実行時には正しく復元できません。

outputFile.write(
    reinterpret_cast<const char*>(&person),
    sizeof(person)
);

文字列は、長さと文字列データを個別に保存する必要があります。

文字列をバイナリ形式で保存する

文字列の長さと本体を書き込む

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

int main()
{
    std::ofstream outputFile(
        "string.bin",
        std::ios::binary
    );

    if (!outputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    const std::string text = "Hello, C++!";
    const std::uint64_t length =
        static_cast<std::uint64_t>(text.size());

    outputFile.write(
        reinterpret_cast<const char*>(&length),
        sizeof(length)
    );

    outputFile.write(
        text.data(),
        static_cast<std::streamsize>(text.size())
    );

    if (!outputFile)
    {
        std::cerr << "書き込みに失敗しました。\n";
        return 1;
    }

    return 0;
}

最初に文字列の長さを書き込み、その後に文字列本体を書き込んでいます。

文字列を読み込む

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

int main()
{
    std::ifstream inputFile(
        "string.bin",
        std::ios::binary
    );

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    std::uint64_t length = 0;

    inputFile.read(
        reinterpret_cast<char*>(&length),
        sizeof(length)
    );

    if (!inputFile)
    {
        std::cerr
            << "文字列長の読み込みに失敗しました。\n";
        return 1;
    }

    constexpr std::uint64_t maxLength =
        10'000'000;

    if (length > maxLength)
    {
        std::cerr
            << "文字列サイズが大きすぎます。\n";
        return 1;
    }

    std::string text(
        static_cast<std::size_t>(length),
        '\0'
    );

    inputFile.read(
        text.data(),
        static_cast<std::streamsize>(text.size())
    );

    if (!inputFile)
    {
        std::cerr
            << "文字列の読み込みに失敗しました。\n";
        return 1;
    }

    std::cout << text << '\n';

    return 0;
}

読み込んだ文字列長をそのまま信用すると、異常に大きなメモリ確保が発生する可能性があります。

そのため、最大サイズを決めて検証することが重要です。

文字列長の保存形式にも注意する

この例では、std::uint64_tのメモリ表現をそのまま保存しています。

そのため、異なる環境間でファイルを共有する場合は、バイト順序に依存します。

移植可能な独自形式にする場合は、次の項目を仕様として決めます。

  • 長さフィールドのサイズ
  • バイト順序
  • 文字列の文字コード
  • 最大文字列長
  • ファイル形式のバージョン

大きなバイナリファイルを分割して読み込む

一定サイズのバッファーを使用する

大きなファイルを一度にメモリへ読み込むと、メモリを大量に消費します。

一定サイズずつ読み込む場合は、次のようにします。

#include <array>
#include <fstream>
#include <iostream>

int main()
{
    std::ifstream inputFile(
        "large.bin",
        std::ios::binary
    );

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    std::array<char, 4096> buffer{};

    while (
        inputFile.read(
            buffer.data(),
            static_cast<std::streamsize>(buffer.size())
        )
        || inputFile.gcount() > 0
    )
    {
        const std::streamsize bytesRead =
            inputFile.gcount();

        std::cout
            << bytesRead
            << "バイト読み込みました。\n";

        // buffer.data()からbytesReadバイト分を処理する
    }

    if (inputFile.bad())
    {
        std::cerr
            << "読み込み中にエラーが発生しました。\n";
        return 1;
    }

    return 0;
}

最後の読み込みでは、バッファーサイズ未満のデータしか残っていない場合があります。

実際に読み込んだバイト数は、gcount()で確認します。

CSVファイルを読み込む

単純なCSVを読み込む

たとえば、users.csvが次の内容だとします。

name,age,city
Alice,25,Tokyo
Bob,30,Osaka

単純な形式であれば、std::getline()を組み合わせて読み込めます。

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

int main()
{
    std::ifstream inputFile("users.csv");

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    std::string line;

    while (std::getline(inputFile, line))
    {
        std::istringstream lineStream(line);

        std::string name;
        std::string age;
        std::string city;

        std::getline(lineStream, name, ',');
        std::getline(lineStream, age, ',');
        std::getline(lineStream, city, ',');

        std::cout
            << name << " / "
            << age << " / "
            << city << '\n';
    }

    return 0;
}

本格的なCSVには対応できない

実際のCSVでは、次のようなデータが含まれることがあります。

"Yamada, Taro",30,"Tokyo"

単純にカンマで分割すると、引用符内のカンマまで区切りとして扱われてしまいます。

本格的なCSVでは、次の仕様を考慮する必要があります。

  • ダブルクォーテーションで囲まれたフィールド
  • フィールド内のカンマ
  • フィールド内の改行
  • ""による引用符のエスケープ
  • 空のフィールド
  • 文字コード
  • 改行コード

実務でCSVを扱う場合は、CSV専用ライブラリを利用する方が安全です。

C++17以降のファイルパス操作

std::filesystemを使用する

C++17以降では、<filesystem>を使ってファイルやディレクトリを操作できます。

#include <filesystem>
#include <iostream>

int main()
{
    const std::filesystem::path path =
        "example.txt";

    if (std::filesystem::exists(path))
    {
        std::cout
            << "ファイルが存在します。\n";
    }
    else
    {
        std::cout
            << "ファイルが存在しません。\n";
    }

    return 0;
}

主な関数は次のとおりです。

std::filesystem::exists(path);
std::filesystem::is_regular_file(path);
std::filesystem::is_directory(path);
std::filesystem::file_size(path);
std::filesystem::remove(path);
std::filesystem::rename(oldPath, newPath);
std::filesystem::create_directory(path);
std::filesystem::create_directories(path);

std::filesystem::pathでパスを組み立てる

#include <filesystem>
#include <fstream>
#include <iostream>

int main()
{
    const std::filesystem::path path =
        std::filesystem::path("data")
        / "example.txt";

    std::ifstream inputFile(path);

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    return 0;
}

/演算子を使うと、環境に適した形でパスを連結できます。

std::filesystem::path("data")
    / "example.txt"

文字列を手作業で連結するより、意図が明確で安全です。

相対パスと絶対パス

相対パス

次のようなパスは相対パスです。

std::ifstream file("data/example.txt");

相対パスは、通常、プログラム実行時のカレントディレクトリを基準に解釈されます。

ソースコードが保存されているディレクトリを基準にするとは限りません。

IDEから実行した場合は、次のような場所がカレントディレクトリになることがあります。

  • プロジェクトのルート
  • ビルドディレクトリ
  • 実行ファイルの出力先
  • IDEで指定された作業ディレクトリ

現在のカレントディレクトリは、次のコードで確認できます。

#include <filesystem>
#include <iostream>

int main()
{
    std::cout
        << std::filesystem::current_path()
        << '\n';
}

絶対パス

Windowsの絶対パスは、次のように記述できます。

std::ifstream file(
    R"(C:\data\example.txt)"
);

通常の文字列では、\がエスケープ文字として解釈されます。

std::ifstream file(
    "C:\\data\\example.txt"
);

生文字列リテラルを使用すると、バックスラッシュを二重に書く必要がありません。

R"(C:\data\example.txt)"

ただし、絶対パスをソースコードへ直接埋め込むと、別の環境で動作しにくくなります。

設定ファイル、コマンドライン引数、ユーザー入力などから受け取る設計も検討します。

日本語ファイル名と文字コード

ファイル名とファイル内容は別の問題

日本語を含むファイル入出力では、次の2つを分けて考える必要があります。

  1. ファイルパスの文字表現
  2. ファイル内容の文字コード

ファイル内容をUTF-8にしても、日本語のファイル名が必ず正しく扱えるとは限りません。

ファイル名は、OSや標準ライブラリが使用するネイティブなパス表現に影響されます。

std::ofstreamは文字コードを自動変換しない

std::ofstreamは、通常、渡された文字列のバイト列を書き込みます。

ストリームが文字列を自動的にUTF-8へ変換するわけではありません。

std::ofstream file("example.txt");
file << "こんにちは";

この文字列がUTF-8のバイト列としてプログラム内に格納されていれば、ファイルにもUTF-8のバイト列が書き込まれます。

ただし、通常の文字列リテラルの文字コードは、次の要素に影響されます。

  • ソースコードの保存形式
  • コンパイラー設定
  • 実行文字集合
  • 開発環境

日本語を安定して扱うには、ソースコードとコンパイラーの文字コード設定を統一する必要があります。

C++20以降のu8文字列

C++20以降では、u8文字列リテラルはchar8_t型になります。

const auto text = u8"こんにちは";

char8_tの文字列は、通常のstd::ofstreamへそのまま<<で出力できない場合があります。

必要に応じて、バイト列として明示的に扱う処理が必要です。

UTF-8のBOMを処理する

UTF-8 BOMとは

UTF-8ファイルの先頭には、BOMと呼ばれる3バイトが付くことがあります。

EF BB BF

BOM付きファイルをそのまま読み込むと、先頭に見えない3バイトが含まれる場合があります。

BOMを除去する例

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

int main()
{
    std::ifstream inputFile(
        "example.txt",
        std::ios::binary
    );

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    std::string content{
        std::istreambuf_iterator<char>(inputFile),
        std::istreambuf_iterator<char>()
    };

    if (
        content.size() >= 3 &&
        static_cast<unsigned char>(content[0]) == 0xEF &&
        static_cast<unsigned char>(content[1]) == 0xBB &&
        static_cast<unsigned char>(content[2]) == 0xBF
    )
    {
        content.erase(0, 3);
    }

    std::cout << content;

    return 0;
}

std::istreambuf_iteratorを使用しているため、<iterator>を忘れずにインクルードします。

ファイルを安全に上書きする

直接上書きする場合の問題

重要なファイルを直接上書きすると、書き込み途中でプログラムが停止した場合に、元のファイルまで失われる可能性があります。

一般的には、次の手順で安全性を高めます。

  1. 一時ファイルへ新しい内容を書き込む
  2. 書き込みとクローズの成功を確認する
  3. 一時ファイルを元のファイルと置き換える

一時ファイルへ保存する例

#include <filesystem>
#include <fstream>
#include <string>

bool saveFileSafely(
    const std::filesystem::path& destination,
    const std::string& content
)
{
    const std::filesystem::path temporary =
        destination.string() + ".tmp";

    {
        std::ofstream outputFile(
            temporary,
            std::ios::binary |
            std::ios::trunc
        );

        if (!outputFile)
        {
            return false;
        }

        outputFile.write(
            content.data(),
            static_cast<std::streamsize>(
                content.size()
            )
        );

        if (!outputFile)
        {
            return false;
        }

        outputFile.close();

        if (!outputFile)
        {
            return false;
        }
    }

    std::error_code error;

    std::filesystem::rename(
        temporary,
        destination,
        error
    );

    if (error)
    {
        std::filesystem::remove(
            temporary,
            error
        );

        return false;
    }

    return true;
}

この方法は、元ファイルへ直接書き込むより安全性を高められます。

ただし、完全な安全性を保証するものではありません。

OSやファイルシステムによる違い

既存ファイルへのrename()の動作や、置換が原子的に行われるかどうかは、OSやファイルシステムによって異なる可能性があります。

実務では、次の点も考慮する必要があります。

  • 一時ファイル名の重複
  • 一時ファイルを同じディレクトリへ作成すること
  • 既存ファイルの権限
  • 同時更新
  • シンボリックリンク
  • 電源断
  • OSや記憶装置のキャッシュ
  • 名前変更処理の原子性

重要なデータでは、対象OSに適したファイル置換APIや同期処理を検討する必要があります。

flush()で出力を反映する

flush()の基本

ファイルへの出力は、効率化のためにストリーム内部のバッファーへ一時的に保持されることがあります。

flush()を呼ぶと、バッファー内のデータを後段へ送り出すよう要求できます。

outputFile << "重要なデータ";
outputFile.flush();

std::endlとの違い

std::endlは、改行を出力した後にフラッシュします。

outputFile
    << "データ"
    << std::endl;

一方、次のコードは通常、改行だけを出力します。

outputFile << "データ\n";

頻繁にフラッシュすると性能が低下する可能性があります。

通常は\nを使用し、即時反映が必要な場面だけflush()を使用します。

flush()は物理保存を保証しない

flush()を呼んでも、電源断に耐えられる状態まで保存されたことは保証されません。

ファイルへの保存には、概念的に次の段階があります。

  1. C++ストリームのバッファー
  2. OSのファイルキャッシュ
  3. ファイルシステム
  4. 記憶装置のキャッシュ
  5. 物理媒体

flush()は、C++ストリームのバッファーを後段へ送る処理ですが、物理媒体への完全保存まで保証するものではありません。

例外を使ってエラー処理する

exceptions()を設定する

標準のファイルストリームは、通常、エラー発生時に例外を投げず、状態フラグを設定します。

exceptions()を設定すると、指定した状態になったときにstd::ios_base::failureを投げられます。

#include <fstream>
#include <iostream>

int main()
{
    try
    {
        std::ifstream inputFile;

        inputFile.exceptions(
            std::ios::failbit |
            std::ios::badbit
        );

        inputFile.open("example.txt");

        int value = 0;
        inputFile >> value;

        std::cout << value << '\n';
    }
    catch (
        const std::ios_base::failure& error
    )
    {
        std::cerr
            << "ファイル入出力エラー: "
            << error.what()
            << '\n';

        return 1;
    }

    return 0;
}

ただし、failbitを例外対象にすると、通常のEOF到達でも例外が発生しやすくなります。

ファイルを最後まで読み込む処理では、状態フラグを使う通常の方法の方が扱いやすい場合があります。

読み込んだデータを検証する

ファイルを開けても内容が正しいとは限らない

ファイルを正常に開けたとしても、中身が正しいとは限りません。

たとえば、年齢を読み込む場合は、整数として読み込めることだけでなく、値の範囲も確認します。

#include <fstream>
#include <iostream>

int main()
{
    std::ifstream inputFile("age.txt");

    if (!inputFile)
    {
        std::cerr << "ファイルを開けませんでした。\n";
        return 1;
    }

    int age = 0;

    if (!(inputFile >> age))
    {
        std::cerr
            << "年齢を整数として読み込めません。\n";
        return 1;
    }

    if (age < 0 || age > 150)
    {
        std::cerr
            << "年齢の値が不正です。\n";
        return 1;
    }

    std::cout
        << "年齢: "
        << age
        << '\n';

    return 0;
}

検証すべき項目

外部ファイルは、基本的に信頼できない入力として扱います。

用途に応じて、次の項目を検証します。

  • 数値の形式
  • 数値の最小値と最大値
  • 文字列の長さ
  • ファイルサイズ
  • レコード数
  • 必須項目の有無
  • データ形式のバージョン
  • マジックナンバー
  • チェックサム
  • 電子署名

実践的なテキストファイル読み込み関数

std::optionalで成功と失敗を表す

#include <filesystem>
#include <fstream>
#include <iterator>
#include <optional>
#include <string>

std::optional<std::string> readTextFile(
    const std::filesystem::path& path
)
{
    std::ifstream inputFile(
        path,
        std::ios::binary
    );

    if (!inputFile)
    {
        return std::nullopt;
    }

    std::string content{
        std::istreambuf_iterator<char>(inputFile),
        std::istreambuf_iterator<char>()
    };

    if (inputFile.bad())
    {
        return std::nullopt;
    }

    return content;
}

使用例は次のとおりです。

#include <iostream>

int main()
{
    const auto content =
        readTextFile("example.txt");

    if (!content)
    {
        std::cerr
            << "ファイルの読み込みに失敗しました。\n";
        return 1;
    }

    std::cout << *content;

    return 0;
}

読み込みに成功した場合は文字列を返し、失敗した場合はstd::nulloptを返します。

実践的なテキストファイル書き込み関数

書き込みとクローズの成功を確認する

#include <filesystem>
#include <fstream>
#include <limits>
#include <string_view>

bool writeTextFile(
    const std::filesystem::path& path,
    std::string_view content
)
{
    if (
        content.size() >
        static_cast<std::size_t>(
            std::numeric_limits<
                std::streamsize
            >::max()
        )
    )
    {
        return false;
    }

    std::ofstream outputFile(
        path,
        std::ios::binary |
        std::ios::trunc
    );

    if (!outputFile)
    {
        return false;
    }

    outputFile.write(
        content.data(),
        static_cast<std::streamsize>(
            content.size()
        )
    );

    if (!outputFile)
    {
        return false;
    }

    outputFile.close();

    return static_cast<bool>(outputFile);
}

使用例は次のとおりです。

#include <iostream>

int main()
{
    const bool success =
        writeTextFile(
            "example.txt",
            "Hello, C++!\n"
        );

    if (!success)
    {
        std::cerr
            << "書き込みに失敗しました。\n";
        return 1;
    }

    return 0;
}

close()後の状態まで確認することで、終了時の書き込みエラーも検出しやすくなります。

C++のファイル入出力でよくある間違い

ファイルを開けたか確認しない

問題のある例です。

std::ifstream file("example.txt");

std::string line;
std::getline(file, line);

ファイルが存在しない、権限がない、パスが間違っているなどの理由で失敗する可能性があります。

次のように確認します。

if (!file)
{
    std::cerr
        << "ファイルを開けませんでした。\n";
    return 1;
}

while (!eof())を使用する

問題のある例です。

while (!file.eof())
{
    file >> value;
}

推奨される書き方は次のとおりです。

while (file >> value)
{
}

相対パスの基準を誤解する

相対パスは、通常、ソースコードの場所ではなく、実行時のカレントディレクトリを基準にします。

std::cout
    << std::filesystem::current_path()
    << '\n';

ファイルが見つからない場合は、まずカレントディレクトリを確認します。

読み込み結果を検証しない

問題のある例です。

int value;
file >> value;
std::cout << value;

読み込みに失敗した場合、valueが有効な値になっているとは限りません。

int value = 0;

if (!(file >> value))
{
    std::cerr
        << "読み込みに失敗しました。\n";
}

バイナリファイルをテキストモードで扱う

画像や独自バイナリ形式などは、std::ios::binaryを指定します。

std::ifstream file(
    "image.png",
    std::ios::binary
);

特にWindowsでは、テキストモードとバイナリモードの違いが問題になることがあります。

ポインターやstd::stringをそのまま保存する

ポインターやstd::stringを含むオブジェクトを、そのままバイナリ保存しても正しく復元できません。

保存形式を明確に設計し、必要なデータを個別に書き込む必要があります。

生の数値形式を移植可能だと思い込む

intdoubleのメモリ表現をそのまま保存すると、型サイズ、バイト順序、内部表現に依存します。

異なる環境間で共有する場合は、保存形式を明確に定義します。

巨大なファイルを一度に読み込む

ファイル全体をstd::stringへ読み込む方法は便利ですが、ファイルサイズと同程度以上のメモリを使用します。

大きなファイルは、行単位または一定サイズのバッファー単位で処理します。

用途別のファイル入出力方法

テキストを1行ずつ読み込む

std::ifstream file("example.txt");

std::string line;

while (std::getline(file, line))
{
}

空白区切りの数値や単語を読み込む

int value = 0;

while (file >> value)
{
}

ファイルへ新規書き込みする

std::ofstream file("example.txt");

ファイル末尾へ追記する

std::ofstream file(
    "example.txt",
    std::ios::app
);

バイナリファイルを読み込む

std::ifstream file(
    "example.bin",
    std::ios::binary
);

読み込みと書き込みの両方を行う

std::fstream file(
    "example.dat",
    std::ios::in |
    std::ios::out
);

ファイル全体を文字列として取得する

std::string content{
    std::istreambuf_iterator<char>(file),
    std::istreambuf_iterator<char>()
};

ファイルサイズを取得する

const auto size =
    std::filesystem::file_size(path);

C++のファイル入出力に関するまとめ

C++のファイル入出力では、主に次の3つのクラスを使用します。

std::ifstream
std::ofstream
std::fstream

基本的な使い分けは次のとおりです。

// 読み込み
std::ifstream inputFile("input.txt");

// 書き込み
std::ofstream outputFile("output.txt");

// 読み込みと書き込み
std::fstream file(
    "data.txt",
    std::ios::in |
    std::ios::out
);

実際のプログラムでは、次の点が重要です。

  • ファイルを開けたか必ず確認する
  • 書き込み後やクローズ後の状態も確認する
  • 読み込み処理そのものをループ条件にする
  • while (!eof())は使用しない
  • 相対パスの基準となるカレントディレクトリを確認する
  • テキストファイルとバイナリファイルを区別する
  • 外部ファイルの内容を信頼せず検証する
  • 大きなファイルは分割して読み込む
  • ポインターやstd::stringをそのままバイナリ保存しない
  • 生の整数保存は型サイズやバイト順序に依存する
  • 重要なファイルは一時ファイル経由で更新する
  • flush()が物理媒体への完全保存を保証するわけではない

C++のファイル入出力を初めて学ぶ場合は、まずstd::ofstreamによる書き込みと、std::ifstreamおよびstd::getline()による1行ずつの読み込みから理解するとよいでしょう。

その後、オープンモード、ストリーム状態、ファイル位置、バイナリ入出力、文字コード、エラー処理の順に学ぶと、実践的なファイル操作を身につけやすくなります。

以上、C++でのファイル入出力についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次