HOWTO · C++

C++ でファイルを文字列に読み込む

この記事では、C++ でファイルを文字列に読み込む方法を説明します。

このページの内容

ファイル全体を std::string に読み込むには、行単位で処理する場合は std::ifstream と std::getline を使い、ストリーム全体を読み込む場合は std::istreambuf_iterator<char> から文字列を構築します。以下の例はそれぞれ一時入力ファイルを作成するため、各プログラムを個別にコンパイルして実行できます。

C++でファイルを文字列に読み込む:std::ifstream と std::getline を使う

std::getline は各行の区切り文字を取り除きます。呼び出しが成功するたびに "\n" を追加すれば区切りを戻せますが、元のファイルが改行で終わっていない場合でも、最後の行の後ろに改行が追加されます。この違いが重要な場合は、区切り文字の有無を記録するか、以下のストリーム全体を読み込む方法を使ってください。

次の完全な例では、一時ファイルに2行を書き込み、std::ifstream で開いて各行を文字列に追加します。コード例の前の説明は、通常どおり成功する場合の使い方を示しています。

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

int main() {
    const auto path = std::filesystem::temp_directory_path() / "cpp-string-lines.txt";
    {
        std::ofstream sample(path);
        sample << "alpha\nbeta";
        if (!sample) {
            std::cout << "Could not create sample file\n";
            return 0;
        }
    }

    std::ifstream inputFile(path);
    if (!inputFile) {
        std::cout << "Could not open sample file\n";
        std::filesystem::remove(path);
        return 0;
    }

    std::string fileContent;
    std::string line;
    while (std::getline(inputFile, line)) {
        fileContent += line + "\n";
    }
    if (inputFile.bad()) {
        std::cout << "Read failed\n";
        inputFile.close();
        std::filesystem::remove(path);
        return 0;
    }

    std::cout << fileContent;
    inputFile.close();
    std::filesystem::remove(path);
}

std::getline が行を取り出せなくなると、ループは終了します。bad() を確認すると、通常のストリーム終端と低レベルのI/Oエラーを区別できます。既定ではファイルはテキストモードで開かれます。改行を変換するシステムでは、結果の文字列に含まれるバイト列がファイル内のものと異なる場合があります。バイト単位で完全に保持する必要がある場合は、バイナリモードを使ってください。

C++でファイルを文字列に読み込む:istreambuf_iterator を使う

ストリーム全体を読み込むには、std::istreambuf_iterator<char> の2つの値から文字列を構築します。最初のイテレーターはストリームバッファから読み込み、デフォルト構築したイテレーターが終端を示します。行に分割せずにデータを読み込むため、ストリームから渡される改行文字も保持されます。

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

int main() {
    const auto path = std::filesystem::temp_directory_path() / "cpp-string-iterator.txt";
    {
        std::ofstream sample(path, std::ios::binary);
        sample << "alpha\nbeta";
        if (!sample) {
            std::cout << "Could not create sample file\n";
            return 0;
        }
    }

    std::ifstream inputFile(path, std::ios::binary);
    if (!inputFile) {
        std::cout << "Could not open sample file\n";
        std::filesystem::remove(path);
        return 0;
    }

    const std::string fileContent{
        std::istreambuf_iterator<char>(inputFile),
        std::istreambuf_iterator<char>()};
    if (inputFile.bad()) {
        std::cout << "Read failed\n";
    } else {
        std::cout << fileContent << '\n';
    }
    inputFile.close();
    std::filesystem::remove(path);
}
alpha
beta

ファイル全体がメモリに収まり、行単位の処理が不要な場合にこの方法を使います。バイナリモードで開けば、テキストモードによる改行変換を避けられます。std::string にはゼロバイトを格納できますが、c_str() をヌル終端テキスト文字列として扱う関数は、最初のゼロバイトで処理を止めることがあります。

C++でファイルを文字列に読み込む:rdbuf を使う

標準ライブラリを使う別の方法として、入力ストリームの rdbuf() を std::ostringstream に挿入し、str() を呼び出して内容を std::string として取得できます。文字列ストリームを周辺のコードですでに使っている場合に便利です。読み取りエラーを報告する必要がある場合は、転送後に入力ストリームを確認してください。

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

int main() {
    const auto path = std::filesystem::temp_directory_path() / "cpp-string-rdbuf.txt";
    {
        std::ofstream sample(path, std::ios::binary);
        sample << "alpha\nbeta";
        if (!sample) {
            std::cout << "Could not create sample file\n";
            return 0;
        }
    }

    std::ifstream inputFile(path, std::ios::binary);
    if (!inputFile) {
        std::cout << "Could not open sample file\n";
        std::filesystem::remove(path);
        return 0;
    }

    std::ostringstream stream;
    stream << inputFile.rdbuf();
    if (inputFile.bad()) {
        std::cout << "Read failed\n";
    } else {
        const std::string fileContent = stream.str();
        std::cout << fileContent << '\n';
    }
    inputFile.close();
    std::filesystem::remove(path);
}

挿入によってファイルバッファの文字が文字列ストリームにコピーされ、str() は蓄積された文字列を返します。ファイル全体から直接文字列を構築するなら、イテレーターを使う方法のほうが手順は少なくなります。どちらの方法でも、ファイルを開けたか、読み取りエラーが起きていないかを確認する必要があります。

ファイルを文字列に読み込む:fread 関数を使う

fread はC標準ライブラリの関数で、C++からも使用できます。コードですでに FILE* ストリームを使っている場合や、バイト単位のC APIが必要な場合に便利です。C++のストリームを使う方法より本質的に高速というわけではなく、性能は処理内容や実装によって異なります。

この例では、シーク可能なサンプルファイルのサイズを fseek と ftell で調べ、そのサイズの文字列を確保してバイトを読み込みます。シークできないストリームや、ファイル全体をメモリに収められない場合は、固定サイズのチャンクに分けて読み込んでください。サイズを測定してから読み込むまでにファイルのサイズが変わることがあるため、返されたバイト数と ferror も確認する必要があります。

#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>

int main() {
    const auto path = std::filesystem::temp_directory_path() / "cpp-string-fread.txt";
    {
        std::ofstream sample(path, std::ios::binary);
        sample << "alpha\nbeta";
        if (!sample) {
            std::cout << "Could not create sample file\n";
            return 0;
        }
    }

    std::FILE* file = std::fopen(path.string().c_str(), "rb");
    if (file == nullptr) {
        std::cout << "Could not open sample file\n";
        std::filesystem::remove(path);
        return 0;
    }

    if (std::fseek(file, 0, SEEK_END) != 0) {
        std::cout << "Could not seek sample file\n";
        std::fclose(file);
        std::filesystem::remove(path);
        return 0;
    }
    const long length = std::ftell(file);
    if (length < 0 || std::fseek(file, 0, SEEK_SET) != 0) {
        std::cout << "Could not determine sample size\n";
        std::fclose(file);
        std::filesystem::remove(path);
        return 0;
    }

    std::string content(static_cast<std::size_t>(length), '\0');
    const std::size_t bytesRead = std::fread(content.data(), 1, content.size(), file);
    const bool failed = std::ferror(file) != 0 || bytesRead != content.size();
    std::fclose(file);
    std::filesystem::remove(path);

    if (failed) {
        std::cout << "Read failed or file changed while reading\n";
        return 0;
    }
    std::cout << content << '\n';
}

第3引数は要素数、第2引数は各要素のサイズです。要素サイズに 1 を指定すると、戻り値はバイト数になります。fread は要求した量より少ないバイト数を返す場合があるため、1回の呼び出しでバッファがすべて埋まるとは限りません。この例はシーク可能なファイルを対象とし、ftell が扱える long の範囲を使っています。より大きなファイルを扱うアプリケーションでは、プラットフォームに適した大容量ファイル用APIを使うか、チャンク単位で読み込んでください。文字列にはバイトが格納されますが、C文字列関数用のヌル終端文字は追加されません。

ファイルを文字列に読み込む:read を使う

POSIXの read 関数は、ファイルディスクリプターと呼び出し側が用意したバッファを使います。標準C++の一部ではないため、適切なPOSIXヘッダーをインクルードし、このAPIを提供するプラットフォームでのみ使用してください。呼び出しが成功しても、要求したバイト数より少ない量しか返らないことがあります。そのため、ファイル終端まで呼び出しを繰り返し、中断やエラーも処理する必要があります。

次のPOSIX専用プログラムは、事前にファイルサイズを測定せず、チャンク単位で読み込みます。使用しているシステムコールはすべてのC++プラットフォームで利用できるわけではないため、移植可能なC++の例ではなく、POSIXの例として扱ってください。

#include <cerrno>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <fcntl.h>
#include <unistd.h>

int main() {
    const auto path = std::filesystem::temp_directory_path() / "cpp-string-read.txt";
    {
        std::ofstream sample(path, std::ios::binary);
        sample << "alpha\nbeta";
        if (!sample) {
            std::cout << "Could not create sample file\n";
            return 0;
        }
    }

    const int fd = ::open(path.c_str(), O_RDONLY);
    if (fd < 0) {
        std::cout << "Could not open sample file\n";
        std::filesystem::remove(path);
        return 0;
    }

    std::string content;
    char buffer[4096];
    bool failed = false;
    for (;;) {
        const ssize_t count = ::read(fd, buffer, sizeof buffer);
        if (count > 0) {
            content.append(buffer, static_cast<std::size_t>(count));
        } else if (count == 0) {
            break;
        } else if (errno != EINTR) {
            failed = true;
            break;
        }
    }
    if (::close(fd) != 0) {
        failed = true;
    }
    std::filesystem::remove(path);

    if (failed) {
        std::cout << "Read or close failed\n";
        return 0;
    }
    std::cout << content << '\n';
}

通常ファイルでは、ファイル終端で read は0を返します。それ以外の種類のファイルディスクリプターでは、動作はデバイスやストリームによって異なります。戻り値が正の場合、その値が追加するバイト数です。負の場合はエラーを示し、EINTR なら呼び出しを再試行できます。ファイルディスクリプターは必ず閉じ、1回の呼び出しで要求したバッファがすべて埋まると仮定せず、部分読み込みに対応してください。この例では std::string::append を使い、ゼロバイトを含めて返されたバイト数どおりに追加します。

まとめ

移植可能なC++でストリーム全体を文字列にする場合は、std::istreambuf_iterator または rdbuf() を選び、行単位の処理が便利な場合は std::getline を使ってください。行の取り出しでは区切り文字が削除され、後から追加すると、改行のない最終行の内容が変わることがあります。周辺のコードがCの FILE* インターフェイスを使っている場合は fread を選び、プラットフォーム固有のファイルディスクリプターAPIが適切な場合に限ってPOSIXの read を使います。大きなファイルでは、全体をメモリに保持せず、チャンク単位で処理する方法も検討してください。ファイルを開く処理と読み取り処理の両方で結果を確認しましょう。